// 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() }