// Package miot provides MIoT core client for Xiaomi Home devices. // It's a Golang port of MIoTClient from xiaomihome-py/miot/. package miot import ( "fmt" "strings" "sync" ) // ============================================================================ // MIoTMatcher (aligned with Python miot/common.py MIoTMatcher) // ============================================================================ // MatcherEntry represents a subscription entry in the matcher. type MatcherEntry struct { ID string Handler func(params map[string]interface{}, ctx interface{}) Ctx interface{} } // MIoTMatcher is a subscription tree for MQTT message dispatching. // It matches topics like "did/p/siid/piid" or "did/e/siid/eiid" // and dispatches to registered handlers. // Aligns with Python miot/common.py MIoTMatcher. type MIoTMatcher struct { routes map[string][]*MatcherEntry mu sync.RWMutex } // NewMIoTMatcher creates a new MIoTMatcher. func NewMIoTMatcher() *MIoTMatcher { return &MIoTMatcher{ routes: make(map[string][]*MatcherEntry), } } // Sub subscribes to a topic pattern. // topic: topic pattern, e.g., "did/p/siid/piid" or "did/p/#" // handler: callback function // ctx: context passed to handler // Returns: subscription ID for unsubscribing. func (m *MIoTMatcher) Sub(topic string, handler func(map[string]interface{}, interface{}), ctx interface{}) string { m.mu.Lock() defer m.mu.Unlock() // Generate subscription ID subID := fmt.Sprintf("%p", handler) entry := &MatcherEntry{ ID: subID, Handler: handler, Ctx: ctx, } m.routes[topic] = append(m.routes[topic], entry) return subID } // Unsub unsubscribes from a topic pattern. // subID: subscription ID returned by Sub. func (m *MIoTMatcher) Unsub(subID string) { m.mu.Lock() defer m.mu.Unlock() for pattern, entries := range m.routes { newEntries := []*MatcherEntry{} for _, entry := range entries { if entry.ID != subID { newEntries = append(newEntries, entry) } } if len(newEntries) == 0 { delete(m.routes, pattern) } else { m.routes[pattern] = newEntries } } } // Match matches a topic and returns all subscription entries. // topic: actual topic (no wildcards), e.g., "did/p/siid/piid" // Returns: slice of matched MatcherEntry. func (m *MIoTMatcher) Match(topic string) []*MatcherEntry { m.mu.RLock() defer m.mu.RUnlock() var results []*MatcherEntry parts := strings.Split(topic, "/") for pattern, entries := range m.routes { if matchTopic(pattern, parts) { results = append(results, entries...) } } return results } // matchTopic checks if a topic matches a pattern. // pattern: may contain + or # wildcards // parts: topic split by "/" func matchTopic(pattern string, parts []string) bool { patternParts := strings.Split(pattern, "/") i := 0 // patternParts index j := 0 // parts index for i < len(patternParts) { p := patternParts[i] if p == "#" { // # matches zero or more levels, and must be the last part return i == len(patternParts)-1 } if j >= len(parts) { return false } if p != "+" && p != parts[j] { return false } i++ j++ } return j == len(parts) } // Clear clears all subscriptions. func (m *MIoTMatcher) Clear() { m.mu.Lock() defer m.mu.Unlock() m.routes = make(map[string][]*MatcherEntry) } // Size returns the number of subscriptions. func (m *MIoTMatcher) Size() int { m.mu.RLock() defer m.mu.RUnlock() count := 0 for _, entries := range m.routes { count += len(entries) } return count } // MatcherNode represents a single route node in the matcher. type MatcherNode struct { Topic string Entries []*MatcherEntry } // IterAllNodes returns a channel that yields all registered route nodes. // The channel is closed after all nodes have been sent. // Aligns with Python: iter_all_nodes(). func (m *MIoTMatcher) IterAllNodes() <-chan *MatcherNode { ch := make(chan *MatcherNode) go func() { m.mu.RLock() defer m.mu.RUnlock() for topic, entries := range m.routes { ch <- &MatcherNode{Topic: topic, Entries: entries} } close(ch) }() return ch } // Get returns entries for an exact topic match (no wildcard expansion). // Returns nil if no matching topic is found. // Aligns with Python: get(topic). func (m *MIoTMatcher) Get(topic string) []*MatcherEntry { m.mu.RLock() defer m.mu.RUnlock() return m.routes[topic] }