feat: 初始化小米 IoT (MIoT) 智能家居 Go 库

- 实现 MIoT 客户端核心功能(MQTT 连接、设备管理、属性读写)
- 支持云端 API 调用与局域网设备发现(mDNS)
- 集成国际化(i18n)多语言支持
- 添加 MIoT 设备规约解析器(spec_parser)
- 包含单元测试与使用示例
This commit is contained in:
2026-06-28 22:05:49 +08:00
parent ac442812b4
commit 3a44cb9e6a
118 changed files with 31630 additions and 0 deletions
+182
View File
@@ -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]
}