feat: 初始化小米 IoT (MIoT) 智能家居 Go 库
- 实现 MIoT 客户端核心功能(MQTT 连接、设备管理、属性读写) - 支持云端 API 调用与局域网设备发现(mDNS) - 集成国际化(i18n)多语言支持 - 添加 MIoT 设备规约解析器(spec_parser) - 包含单元测试与使用示例
This commit is contained in:
@@ -0,0 +1,274 @@
|
||||
// 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"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ============================================================================
|
||||
// Property refresh (aligned with Python miot_client.py)
|
||||
// ============================================================================
|
||||
|
||||
// RefreshDeviceAllProps enqueues all readable properties of a device for refresh.
|
||||
// Iterates device.ServiceList → service.Properties → RequestRefreshProp for each readable prop.
|
||||
// Aligns with Python: MIoTDevice.__refresh_props_value().
|
||||
func (c *MIoTClient) RefreshDeviceAllProps(did string) error {
|
||||
c.deviceListMu.RLock()
|
||||
info, exists := c.deviceListCache[did]
|
||||
c.deviceListMu.RUnlock()
|
||||
if !exists {
|
||||
return fmt.Errorf("device %s not found in cache", did)
|
||||
}
|
||||
|
||||
device := c.getOrCreateDevice(did, info)
|
||||
if device == nil {
|
||||
return fmt.Errorf("device %s: SPEC not available (set specParser first)", did)
|
||||
}
|
||||
|
||||
count := 0
|
||||
for _, svc := range device.ServiceList() {
|
||||
if svc == nil {
|
||||
continue
|
||||
}
|
||||
for _, prop := range svc.Properties {
|
||||
if prop == nil || !prop.Readable() {
|
||||
continue
|
||||
}
|
||||
c.RequestRefreshProp(did, prop.SIID, prop.PIID)
|
||||
count++
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf("[MIoTClient] RefreshDeviceAllProps: %s → %d props enqueued\n", did, count)
|
||||
return nil
|
||||
}
|
||||
|
||||
// RequestRefreshProp requests to refresh a device property.
|
||||
// Aligns with Python: request_refresh_prop method.
|
||||
// After calling this method, the property will be fetched from cloud in the next refresh cycle.
|
||||
func (c *MIoTClient) RequestRefreshProp(did string, siid, piid int) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
key := fmt.Sprintf("%s|%d|%d", did, siid, piid)
|
||||
|
||||
// Check if property is readable (TODO: implement readability check)
|
||||
// For now, assume all properties are readable
|
||||
|
||||
// Check if already in refresh list
|
||||
if _, exists := c.refreshPropsList[key]; exists {
|
||||
fmt.Printf("[MIoTClient] RequestRefreshProp: SKIPPED (already queued) %s\n", key)
|
||||
return
|
||||
}
|
||||
|
||||
c.refreshPropsList[key] = map[string]interface{}{
|
||||
"did": did,
|
||||
"siid": siid,
|
||||
"piid": piid,
|
||||
}
|
||||
|
||||
// Schedule refresh if not already scheduled
|
||||
if c.refreshPropsTimer == nil {
|
||||
fmt.Printf("[MIoTClient] RequestRefreshProp: Scheduling refresh timer for %s\n", key)
|
||||
// Reset retry count for new batch
|
||||
c.refreshRetryCount = 0
|
||||
|
||||
c.refreshPropsTimer = time.AfterFunc(RefreshPropsDelay*time.Millisecond, func() {
|
||||
c.mu.Lock()
|
||||
c.refreshPropsTimer = nil
|
||||
c.mu.Unlock()
|
||||
|
||||
fmt.Printf("[MIoTClient] RequestRefreshProp: Timer fired, calling refreshPropsHandler()\n")
|
||||
c.refreshPropsHandler()
|
||||
})
|
||||
} else {
|
||||
fmt.Printf("[MIoTClient] RequestRefreshProp: Timer already scheduled, added to queue: %s\n", key)
|
||||
}
|
||||
|
||||
fmt.Printf("[MIoTClient] RequestRefreshProp: QUEUED %s (queue size: %d)\n", key, len(c.refreshPropsList))
|
||||
}
|
||||
|
||||
// refreshPropsHandler handles property refresh from cloud.
|
||||
// Aligns with Python: __refresh_props_handler method.
|
||||
func (c *MIoTClient) refreshPropsHandler() {
|
||||
c.mu.Lock()
|
||||
if len(c.refreshPropsList) == 0 {
|
||||
c.mu.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
// Collect properties to refresh
|
||||
propsToRefresh := make([]map[string]interface{}, 0, len(c.refreshPropsList))
|
||||
for _, prop := range c.refreshPropsList {
|
||||
propsToRefresh = append(propsToRefresh, prop)
|
||||
}
|
||||
c.refreshPropsList = make(map[string]map[string]interface{})
|
||||
c.mu.Unlock()
|
||||
|
||||
fmt.Printf("[MIoTClient] refreshPropsHandler: START, %d props to refresh\n", len(propsToRefresh))
|
||||
|
||||
// Group props by source (cloud / gateway / lan)
|
||||
cloudParams := make([]PropParam, 0)
|
||||
gatewayParams := make(map[string][]PropParam) // groupID -> params
|
||||
lanParams := make([]PropParam, 0)
|
||||
|
||||
c.deviceListMu.RLock()
|
||||
for _, prop := range propsToRefresh {
|
||||
did, _ := prop["did"].(string)
|
||||
if did == "" {
|
||||
continue
|
||||
}
|
||||
siid, _ := prop["siid"].(int)
|
||||
piid, _ := prop["piid"].(int)
|
||||
|
||||
param := PropParam{DID: did, Siid: siid, Piid: piid}
|
||||
|
||||
source, exists := c.subSourceList[did]
|
||||
if !exists {
|
||||
source = "cloud"
|
||||
}
|
||||
|
||||
switch source {
|
||||
case "cloud":
|
||||
cloudParams = append(cloudParams, param)
|
||||
case "lan":
|
||||
lanParams = append(lanParams, param)
|
||||
default:
|
||||
// Gateway: source is the groupID
|
||||
gatewayParams[source] = append(gatewayParams[source], param)
|
||||
}
|
||||
}
|
||||
c.deviceListMu.RUnlock()
|
||||
|
||||
allResults := make([]PropResult, 0)
|
||||
|
||||
// Cloud source: batch fetch
|
||||
if len(cloudParams) > 0 && c.http != nil {
|
||||
// Batch by 150
|
||||
for i := 0; i < len(cloudParams); i += 150 {
|
||||
end := i + 150
|
||||
if end > len(cloudParams) {
|
||||
end = len(cloudParams)
|
||||
}
|
||||
batch := cloudParams[i:end]
|
||||
results, err := c.http.GetProps(batch)
|
||||
if err != nil {
|
||||
fmt.Printf("[MIoTClient] refreshPropsHandler: cloud GetProps FAILED: %v\n", err)
|
||||
c.retryProps(propsToRefresh)
|
||||
return
|
||||
}
|
||||
allResults = append(allResults, results...)
|
||||
fmt.Printf("[MIoTClient] refreshPropsHandler: cloud batch %d returned %d results\n", i/150+1, len(results))
|
||||
}
|
||||
}
|
||||
|
||||
// Gateway source: group by groupID
|
||||
for groupID, params := range gatewayParams {
|
||||
mips, exists := c.mipsLocal[groupID]
|
||||
if !exists {
|
||||
fmt.Printf("[MIoTClient] refreshPropsHandler: gateway %s not found, routing to cloud\n", groupID)
|
||||
// Fallback to cloud
|
||||
if c.http != nil {
|
||||
results, err := c.http.GetProps(params)
|
||||
if err == nil {
|
||||
allResults = append(allResults, results...)
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
results, err := mips.GetProps(params, MipsRequestTimeoutDefault*time.Millisecond)
|
||||
if err != nil {
|
||||
fmt.Printf("[MIoTClient] refreshPropsHandler: gateway %s GetProps FAILED: %v\n", groupID, err)
|
||||
// Fallback to cloud
|
||||
if c.http != nil {
|
||||
results, err := c.http.GetProps(params)
|
||||
if err == nil {
|
||||
allResults = append(allResults, results...)
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
allResults = append(allResults, results...)
|
||||
fmt.Printf("[MIoTClient] refreshPropsHandler: gateway %s returned %d results\n", groupID, len(results))
|
||||
}
|
||||
|
||||
// LAN source
|
||||
if len(lanParams) > 0 && c.miotLan != nil {
|
||||
lan, ok := c.miotLan.(*MIoTLan)
|
||||
if ok {
|
||||
results, err := lan.GetProps(lanParams)
|
||||
if err != nil {
|
||||
fmt.Printf("[MIoTClient] refreshPropsHandler: LAN GetProps FAILED: %v\n", err)
|
||||
// Fallback to cloud
|
||||
if c.http != nil {
|
||||
results, err := c.http.GetProps(lanParams)
|
||||
if err == nil {
|
||||
allResults = append(allResults, results...)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
allResults = append(allResults, results...)
|
||||
fmt.Printf("[MIoTClient] refreshPropsHandler: LAN returned %d results\n", len(results))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Success - reset retry count
|
||||
c.mu.Lock()
|
||||
c.refreshRetryCount = 0
|
||||
c.mu.Unlock()
|
||||
|
||||
fmt.Printf("[MIoTClient] refreshPropsHandler: total %d results\n", len(allResults))
|
||||
|
||||
// Dispatch results to subscribers
|
||||
for _, result := range allResults {
|
||||
if result.Code != 0 {
|
||||
fmt.Printf("[MIoTClient] refreshPropsHandler: FAILED %s/%d/%d, code=%d\n", result.DID, result.Siid, result.Piid, result.Code)
|
||||
continue
|
||||
}
|
||||
|
||||
fmt.Printf("[MIoTClient] refreshPropsHandler: SUCCESS %s/%d/%d = %v\n", result.DID, result.Siid, result.Piid, result.Value)
|
||||
params := map[string]interface{}{
|
||||
"did": result.DID,
|
||||
"siid": result.Siid,
|
||||
"piid": result.Piid,
|
||||
"value": result.Value,
|
||||
}
|
||||
c.OnPropMsg(params, nil)
|
||||
}
|
||||
}
|
||||
|
||||
// retryProps re-queues prop requests for retry.
|
||||
func (c *MIoTClient) retryProps(props []map[string]interface{}) {
|
||||
c.mu.Lock()
|
||||
c.refreshRetryCount++
|
||||
retryCount := c.refreshRetryCount
|
||||
c.mu.Unlock()
|
||||
|
||||
if retryCount >= RefreshPropsRetryMax {
|
||||
fmt.Printf("[MIoTClient] refreshPropsHandler: GIVING UP after %d retries\n", retryCount)
|
||||
c.mu.Lock()
|
||||
c.refreshPropsList = make(map[string]map[string]interface{})
|
||||
c.refreshRetryCount = 0
|
||||
c.mu.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("[MIoTClient] refreshPropsHandler: RETRYING (%d/%d)\n", retryCount, RefreshPropsRetryMax)
|
||||
c.mu.Lock()
|
||||
for _, prop := range props {
|
||||
key := fmt.Sprintf("%s|%d|%d", prop["did"].(string), prop["siid"].(int), prop["piid"].(int))
|
||||
c.refreshPropsList[key] = prop
|
||||
}
|
||||
if c.refreshPropsTimer == nil {
|
||||
c.refreshPropsTimer = time.AfterFunc(RefreshPropsRetryDelay*time.Millisecond, func() {
|
||||
c.mu.Lock()
|
||||
c.refreshPropsTimer = nil
|
||||
c.mu.Unlock()
|
||||
c.refreshPropsHandler()
|
||||
})
|
||||
}
|
||||
c.mu.Unlock()
|
||||
}
|
||||
Reference in New Issue
Block a user