- 实现 MIoT 客户端核心功能(MQTT 连接、设备管理、属性读写) - 支持云端 API 调用与局域网设备发现(mDNS) - 集成国际化(i18n)多语言支持 - 添加 MIoT 设备规约解析器(spec_parser) - 包含单元测试与使用示例
183 lines
4.3 KiB
Go
183 lines
4.3 KiB
Go
// 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]
|
|
}
|