feat: 初始化小米 IoT (MIoT) 智能家居 Go 库
- 实现 MIoT 客户端核心功能(MQTT 连接、设备管理、属性读写) - 支持云端 API 调用与局域网设备发现(mDNS) - 集成国际化(i18n)多语言支持 - 添加 MIoT 设备规约解析器(spec_parser) - 包含单元测试与使用示例
This commit is contained in:
@@ -0,0 +1,182 @@
|
||||
// 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]
|
||||
}
|
||||
Reference in New Issue
Block a user