Files
xiaomihome/miot/miot_client_prop.go
T
4566704 b808e51b3e refactor: 重大架构升级 — HomeID/RoomID int64化 + 设备驱动重构 + 结构化日志
主要变更:

**类型系统重构**
- HomeInfo.ID/RoomInfo.ID/HomeID 从 string 改为 int64,json tag 使用 ,string
- GetHome/GetRooms/GetRoomDevices 接口同步修改
- 新增 int64FromStr 工具函数,更新 parseHomeInfo 解析逻辑

**新增 Speaker 设备**
- 新增 KindSpeaker 类型
- classify 映射覆盖 speaker/bluetooth-speaker/alarm-clock/media-player/playback/music

**设备驱动重构**
- AC 驱动彻底重写:从硬编码常量改为基于 SPEC ValueList 的动态 valMapper
- 所有设备(Fan/Humidifier/Light/OccupancySensor/Switch/TempHumiditySensor/Thermostat/Vacuum/WaterHeater/Cover)新增 GetState() 和 FetchState(ctx) 接口
- base.go 新增 specResolver.format() 方法

**MIoT 模块增强**
- MIoTClient 新增 homeIDs 字段,支持按家庭过滤设备
- RefreshDevices 支持 homeIDs 参数
- SpecStdLib 引入 logger.Logger,log.Printf 全部迁移为结构化日志

**其他**
- Config 新增 RedirectURL 和 HomeIDs 字段
- AuthInfo.ExpiresIn 类型从 int 改为 int32
- UUID 生成简化为 hex.EncodeToString
- 新增 github.com/sirupsen/logrus 依赖
- .gitignore 新增 examples/*/data/ 忽略规则
- 删除已提交的 config.json 敏感配置
2026-07-13 10:40:20 +08:00

307 lines
8.9 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"
"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++
}
}
c.lgr.Infof("[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 {
c.lgr.Infof("[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 {
c.lgr.Infof("[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()
c.lgr.Infof("[MIoTClient] RequestRefreshProp: Timer fired, calling refreshPropsHandler()\n")
c.refreshPropsHandler()
})
} else {
c.lgr.Infof("[MIoTClient] RequestRefreshProp: Timer already scheduled, added to queue: %s\n", key)
}
c.lgr.Infof("[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()
c.lgr.Infof("[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)
// siid=0 && piid=0 → 通配符,展开为设备所有可读属性
if siid == 0 && piid == 0 {
if info, ok := c.deviceListCache[did]; ok {
dev := c.getOrCreateDevice(did, info)
if dev != nil {
for _, svc := range dev.ServiceList() {
if svc == nil {
continue
}
for _, p := range svc.Properties {
if p == nil || !p.Readable() {
continue
}
param := PropParam{DID: did, Siid: p.SIID, Piid: p.PIID}
source := c.subSourceList[did]
if source == "" {
source = "cloud"
}
if source == "cloud" {
cloudParams = append(cloudParams, param)
} else if source == "lan" {
lanParams = append(lanParams, param)
} else {
gatewayParams[source] = append(gatewayParams[source], param)
}
}
}
}
}
continue
}
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 {
c.lgr.Errorf("[MIoTClient] refreshPropsHandler: cloud GetProps FAILED: %v\n", err)
c.retryProps(propsToRefresh)
return
}
allResults = append(allResults, results...)
c.lgr.Infof("[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 {
c.lgr.Infof("[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 {
c.lgr.Errorf("[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...)
c.lgr.Infof("[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 {
c.lgr.Errorf("[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...)
c.lgr.Infof("[MIoTClient] refreshPropsHandler: LAN returned %d results\n", len(results))
}
}
}
// Success - reset retry count
c.mu.Lock()
c.refreshRetryCount = 0
c.mu.Unlock()
c.lgr.Infof("[MIoTClient] refreshPropsHandler: total %d results\n", len(allResults))
// Dispatch results to subscribers
for _, result := range allResults {
if result.Code != 0 {
c.lgr.Errorf("[MIoTClient] refreshPropsHandler: FAILED %s/%d/%d, code=%d\n", result.DID, result.Siid, result.Piid, result.Code)
continue
}
c.lgr.Infof("[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 {
c.lgr.Infof("[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
}
c.lgr.Infof("[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()
}