// Package miot provides MIoT core client for Xiaomi Home devices. // It's a Golang port of MIoTClient from xiaomihome-py/miot/. package miot import ( "context" "encoding/json" "fmt" "sync" "time" "xiaomihome/logger" ) // ============================================================================ // MIoTClient (aligned with Python miot_client.py MIoTClient) // ============================================================================ // MIoTClient is the main MIoT client that manages devices, MQTT, cloud API, subscriptions, and property refresh. // Aligns with Python miot_client.py MIoTClient. // One struct to rule them all - no DeviceManager layer. type MIoTClient struct { // === Basic configuration (aligned with Python __init__ parameters) === entryID string entryData map[string]interface{} uid string cloudServer string ctrlMode CtrlMode // === Network & Storage & Auth (aligned with Python) === network interface{} // MIoTNetwork (optional, for network status monitoring) storage *MIoTStorage mipsSvc interface{} // MipsService (optional, for mDNS service discovery) http *MIoTHttpClient cert *MIoTCert miotLan interface{} // MIoTLan (optional, for LAN control) // === OAuth client (aligned with Python miot_cloud.py MIoTOauthClient) === oauth *MIoTOauthClient homeIDs []string // 需要管理的家庭 ID 列表,nil/空=全部 // === Device list (aligned with Python _device_list_cache etc.) === // Only store metadata map[string]interface{}, not *MIoTDevice objects // Python: _device_list_cache: dict[str, dict] deviceListCache map[string]map[string]interface{} deviceListCloud map[string]map[string]interface{} deviceListGateway map[string]map[string]interface{} deviceListLan map[string]map[string]interface{} deviceListUpdateTs int64 deviceListMu sync.RWMutex // === Subscription related (aligned with Python _sub_source_list / _sub_tree / _sub_device_state) === subSourceList map[string]string // did -> source ("cloud"/"lan"/group_id) subTree *MIoTMatcher // Property/event subscription tree subDeviceState map[string]*MipsDeviceState deviceStateSubs map[string]*deviceStateSub // did -> handler (H4) // === Property refresh (aligned with Python _refresh_props_list etc.) === refreshPropsList map[string]map[string]interface{} // key="did|siid|piid" refreshPropsTimer *time.Timer refreshRetryCount int // === i18n (aligned with Python MIoTI18n) === i18n *MIoTI18n // === SPEC parser + device cache (aligned with Python) === specParser *MIoTSpecParser devices map[string]*MIoTDevice // did → *MIoTDevice instance cache // === Timers (aligned with Python) === refreshCertTimer *time.Timer refreshCloudDevicesTimer *time.Timer mipsLocalStateTimers map[string]*time.Timer showDevicesChangedNotifyTimer *time.Timer // === MIPS clients (aligned with Python _mips_cloud / _mips_local) === mipsCloud *MipsCloudClient mipsLocal map[string]*MipsLocalClient // group_id -> client // === Callback (aligned with Python _persistence_notify) === persistenceNotify func(notifyID, title, message string) // === Config properties (M1: aligned with Python MIoTClient attributes) === ActionDebug bool HideNonStandardEntities bool DisplayBinaryText bool DisplayBinaryBool bool CoverDeadZoneWidth int DisplayDevicesChangedNotify []string PersistentNotify func(string, string, string) // notifyID, title, message // === Cloud API helpers === // These fields align with Python miot_cloud.py MIoTHttpClient methods // They are used for device property read/write and action execution cloudAPIMu sync.RWMutex // === Run control === // Go uses context.Context instead of Python's asyncio loop ctx context.Context cancel context.CancelFunc mu sync.RWMutex refreshMu sync.Mutex // token 刷新互斥锁 // === Logger === lgr logger.Logger } // NewMIoTClient creates a new MIoTClient. // Aligns with Python MIoTClient.__init__. func NewMIoTClient(entryID string, entryData map[string]interface{}, uid string, cloudServer string, ctrlMode CtrlMode) *MIoTClient { ctx, cancel := context.WithCancel(context.Background()) c := &MIoTClient{ entryID: entryID, entryData: entryData, uid: uid, cloudServer: cloudServer, ctrlMode: ctrlMode, deviceListCache: make(map[string]map[string]interface{}), deviceListCloud: make(map[string]map[string]interface{}), deviceListGateway: make(map[string]map[string]interface{}), deviceListLan: make(map[string]map[string]interface{}), subSourceList: make(map[string]string), subTree: NewMIoTMatcher(), subDeviceState: make(map[string]*MipsDeviceState), deviceStateSubs: make(map[string]*deviceStateSub), refreshPropsList: make(map[string]map[string]interface{}), mipsLocal: make(map[string]*MipsLocalClient), mipsLocalStateTimers: make(map[string]*time.Timer), ctx: ctx, cancel: cancel, lgr: logger.Default(), } return c } // SetLogger sets a custom logger for MIoTClient. func (c *MIoTClient) SetLogger(l logger.Logger) { c.lgr = l } // Init initializes the MIoTClient. // Aligns with Python MIoTClient.init_async. func (c *MIoTClient) Init() error { // 1. Load user config from storage, fallback to entryData accessToken, _ := c.entryData["access_token"].(string) refreshToken, _ := c.entryData["refresh_token"].(string) if c.storage != nil { authInfo := c.storage.LoadUserConfig(c.uid, c.cloudServer, nil) if t, ok := authInfo["access_token"].(string); ok && t != "" { accessToken = t // Also restore refresh_token and expires_ts from storage if rt, ok := authInfo["refresh_token"].(string); ok && rt != "" { refreshToken = rt c.entryData["refresh_token"] = rt } if et, ok := authInfo["expires_ts"].(float64); ok { c.entryData["expires_ts"] = et } c.lgr.Infof("[MIoTClient] Loaded token from storage\n") } else if accessToken != "" { // First run: write entryData token to storage expiresTS := float64(time.Now().Unix()) + 3600*24*30 // default 30 days if et, ok := c.entryData["expires_ts"].(float64); ok && et > 0 { expiresTS = et } c.storage.UpdateUserConfig(c.uid, c.cloudServer, map[string]interface{}{ "access_token": accessToken, "refresh_token": refreshToken, "expires_ts": expiresTS, }, true) c.lgr.Infof("[MIoTClient] Saved initial token to storage\n") } } // Keep access_token in entryData for downstream use if c.entryData != nil { c.entryData["access_token"] = accessToken } // 2. Load device cache if err := c.loadCacheDevice(); err != nil { c.lgr.Warnf("[MIoTClient] Warning: failed to load device cache: %v\n", err) } // 3.1 Initialize OAuth client if c.oauth == nil { oauthRedirectURL, _ := c.entryData["oauth_redirect_url"].(string) uuid, _ := c.entryData["uuid"].(string) c.oauth = NewMIoTOauthClient(OAUTH2_CLIENT_ID, oauthRedirectURL, c.cloudServer, uuid) c.lgr.Infof("[MIoTClient] OAuth client initialized\n") } // 3.2 Initialize Cert client cert, err := NewMIoTCert(c.storage, c.uid, c.cloudServer) if err != nil { c.lgr.Warnf("[MIoTClient] Warning: cert init failed: %v\n", err) } else { c.cert = cert c.cert.VerifyCACert() c.lgr.Infof("[MIoTClient] Cert client initialized\n") } // 4. Initialize HTTP client (use token from storage if available) if c.http == nil && accessToken != "" { httpClient, err := NewMIoTHttpClient(c.cloudServer, OAUTH2_CLIENT_ID, accessToken) if err != nil { c.lgr.Warnf("[MIoTClient] Warning: failed to create HTTP client: %v\n", err) } else { c.http = httpClient c.lgr.Infof("[MIoTClient] HTTP client created\n") } } // 3.3 Initialize MipsCloudClient (use token from storage if available) if c.mipsCloud == nil && accessToken != "" { uuid, _ := c.entryData["uuid"].(string) if uuid != "" { broker := buildCloudBroker(c.cloudServer) mips := NewMipsCloudClient(broker, "ha."+uuid, OAUTH2_CLIENT_ID, accessToken) key := c.uid + "-" + c.cloudServer mips.SubState(key, c.onMipsCloudStateChanged) c.mipsCloud = mips // Connect asynchronously to not block Init c.mu.Unlock() if err := mips.Connect(); err != nil { c.lgr.Warnf("[MIoTClient] Warning: MIPS cloud connect failed: %v\n", err) } else { c.lgr.Infof("[MIoTClient] MIPS cloud connected\n") } c.mu.Lock() } } // 3.4 Register network status callback if c.network != nil { nw, ok := c.network.(*MIoTNetwork) if ok { key := c.uid + "-" + c.cloudServer nw.SubNetworkStatus(key, c.onNetworkStatusChanged) status := nw.GetNetworkStatus() c.onNetworkStatusChanged(status) c.lgr.Infof("[MIoTClient] Network status subscribed (online=%v)\n", status) } } // 3.5 LAN control initialization if c.ctrlMode == CtrlModeAuto { key := c.uid + "-" + c.cloudServer // Central hub gateway ctrl if containsStr(SUPPORT_CENTRAL_GATEWAY_CTRL, c.cloudServer) { // mDNS service subscription if c.mipsSvc != nil { svc, ok := c.mipsSvc.(*MipsService) if ok { homeSelected, _ := c.entryData["home_selected"].(map[string]interface{}) for homeID, hi := range homeSelected { info, _ := hi.(map[string]interface{}) groupID, _ := info["group_id"].(string) if groupID != "" { svc.SubServiceChange(key, groupID, c.onMipsServiceStateChange) c.lgr.Infof("[MIoTClient] mDNS service subscribed: home=%s, group=%s\n", homeID, groupID) // Check if service already discovered serviceData := svc.GetServices(groupID) if sd, exists := serviceData[groupID]; exists && sd.validService() { c.lgr.Infof("[MIoTClient] Central mips service scanned: %s, %v\n", homeID, sd) mips := NewMipsLocalClient( getVirtualDID(c.entryData), sd.Addresses[0], sd.Port, c.cert.CAFile(), c.cert.CertFile(), c.cert.KeyFile(), ) mips.GroupID = groupID mips.OnDevListChanged = c.onGWDeviceListChanged mips.SubState(groupID, c.onMipsLocalStateChanged) mips.Connect() c.mipsLocal[groupID] = mips } } } } } } // LAN vote + LAN state subscription if c.miotLan != nil { lan, ok := c.miotLan.(*MIoTLan) if ok { lan.VoteForLanCtrl(key, true) lan.SubLanState(key, c.onMiotLanStateChange) // Wire LAN device state callback lan.SubDeviceState("miot_client", c.onLanDeviceStateChanged, nil) if lan.InitDone() { c.onMiotLanStateChange(true) } c.lgr.Infof("[MIoTClient] LAN control initialized\n") } } } else { // Non-auto mode: cancel LAN if c.miotLan != nil { key := c.uid + "-" + c.cloudServer lan, ok := c.miotLan.(*MIoTLan) if ok { lan.UnsubLanState(key) if lan.InitDone() { lan.UnsubDeviceState(key) dids := make([]string, 0, len(c.deviceListCache)) for did := range c.deviceListCache { dids = append(dids, did) } lan.DeleteDevices(dids) } lan.VoteForLanCtrl(key, false) } } } // 3.6 Initialize i18n lang, _ := c.entryData["integration_language"].(string) if lang == "" { lang = DEFAULT_INTEGRATION_LANGUAGE } c.i18n = NewMIoTI18n(lang) c.i18n.Init() c.lgr.Infof("[MIoTClient] i18n initialized (lang=%s)\n", lang) c.lgr.Infof("[MIoTClient] Initialized\n") return nil } // Deinit deinitializes the MIoTClient. // Aligns with Python MIoTClient.deinit_async. func (c *MIoTClient) Deinit() error { c.mu.Lock() defer c.mu.Unlock() key := c.uid + "-" + c.cloudServer // 1. Network unsub if c.network != nil { nw, ok := c.network.(*MIoTNetwork) if ok { nw.UnsubNetworkStatus(key) } } // 2. Cancel refresh props timer + clear list if c.refreshPropsTimer != nil { c.refreshPropsTimer.Stop() c.refreshPropsTimer = nil } c.refreshPropsList = make(map[string]map[string]interface{}) c.refreshRetryCount = 0 // 3. MipsCloud: unsub → disconnect if c.mipsCloud != nil { c.mipsCloud.UnsubState(key) c.mipsCloud.Disconnect() c.lgr.Infof("[MIoTClient] MIPS cloud disconnected\n") } // 4. Cancel refresh cloud devices timer if c.refreshCloudDevicesTimer != nil { c.refreshCloudDevicesTimer.Stop() c.refreshCloudDevicesTimer = nil } // 5. MipsLocal: unsub → disconnect + clear mDNS subs if c.ctrlMode == CtrlModeAuto { if containsStr(SUPPORT_CENTRAL_GATEWAY_CTRL, c.cloudServer) { if c.mipsSvc != nil { svc, ok := c.mipsSvc.(*MipsService) if ok { svc.UnsubServiceChange(key) } } for _, mips := range c.mipsLocal { mips.OnDevListChanged = nil mips.UnsubState(mips.GroupID) mips.Disconnect() } c.mipsLocal = make(map[string]*MipsLocalClient) // Cancel mips local state timers for _, timer := range c.mipsLocalStateTimers { timer.Stop() } c.mipsLocalStateTimers = make(map[string]*time.Timer) } // LAN cleanup if c.miotLan != nil { lan, ok := c.miotLan.(*MIoTLan) if ok { lan.UnsubLanState(key) if lan.InitDone() { lan.UnsubDeviceState(key) dids := make([]string, 0, len(c.deviceListCache)) for did := range c.deviceListCache { dids = append(dids, did) } lan.DeleteDevices(dids) } lan.VoteForLanCtrl(key, false) } } } // 6. Stop cert timers if c.refreshCertTimer != nil { c.refreshCertTimer.Stop() c.refreshCertTimer = nil } // 7. Cancel show devices changed notify timer if c.showDevicesChangedNotifyTimer != nil { c.showDevicesChangedNotifyTimer.Stop() c.showDevicesChangedNotifyTimer = nil } // 8. OAuth close if c.oauth != nil { c.oauth.Close() } // 9. HTTP close if c.http != nil { c.http.Close() } // 10. i18n deinit if c.i18n != nil { c.i18n.Deinit() } // 11. Save device cache to storage if c.storage != nil && len(c.deviceListCache) > 0 { deviceListForStorage := make(map[string]interface{}) for did, info := range c.deviceListCache { deviceListForStorage[did] = info } deviceKey := fmt.Sprintf("%s_%s", c.uid, c.cloudServer) if err := c.storage.Save("miot_devices", deviceKey, deviceListForStorage); err != nil { c.lgr.Warnf("[MIoTClient] Warning: failed to save devices on deinit: %v\n", err) } c.lgr.Infof("[MIoTClient] Device cache saved to storage\n") } // Cancel context if c.cancel != nil { c.cancel() } // Clear subscription tree c.subTree.Clear() c.lgr.Infof("[MIoTClient] Deinitialized\n") return nil } // loadCacheDevice loads device cache from storage. // Aligns with Python __load_cache_device_async. func (c *MIoTClient) loadCacheDevice() error { c.deviceListMu.Lock() defer c.deviceListMu.Unlock() if c.storage == nil { return fmt.Errorf("storage not available") } // Load device list from storage deviceKey := fmt.Sprintf("%s_%s", c.uid, c.cloudServer) rawDeviceList, err := c.storage.Load("miot_devices", deviceKey, "dict") if err != nil { return err } deviceList, _ := rawDeviceList.(map[string]interface{}) if deviceList == nil { deviceList = make(map[string]interface{}) } // Reset online status to false for did, deviceInfo := range deviceList { if info, ok := deviceInfo.(map[string]interface{}); ok { info["online"] = false deviceList[did] = info } } // Convert map[string]interface{} to map[string]map[string]interface{} for did, info := range deviceList { if infoMap, ok := info.(map[string]interface{}); ok { c.deviceListCache[did] = infoMap } } c.lgr.Infof("[MIoTClient] Loaded %d devices from cache\n", len(deviceList)) return nil } // SetStorage sets the storage client. func (c *MIoTClient) SetStorage(storage *MIoTStorage) { c.mu.Lock() defer c.mu.Unlock() c.storage = storage } // SetHTTPClient sets the HTTP client. func (c *MIoTClient) SetHTTPClient(http *MIoTHttpClient) { c.mu.Lock() defer c.mu.Unlock() c.http = http } // SetMipsCloud sets the cloud MIPS client. func (c *MIoTClient) SetMipsCloud(client *MipsCloudClient) { c.mu.Lock() defer c.mu.Unlock() c.mipsCloud = client } // SetSpecParser sets the SPEC parser for device property discovery. func (c *MIoTClient) SetSpecParser(p *MIoTSpecParser) { c.mu.Lock() defer c.mu.Unlock() c.specParser = p } // SetMipsLocal sets a local MIPS client. func (c *MIoTClient) SetMipsLocal(groupID string, client *MipsLocalClient) { c.mu.Lock() defer c.mu.Unlock() c.mipsLocal[groupID] = client } // DeviceList returns the device list cache. // Aligns with Python device_list property. func (c *MIoTClient) DeviceList() map[string]map[string]interface{} { c.deviceListMu.RLock() defer c.deviceListMu.RUnlock() // Return a copy result := make(map[string]map[string]interface{}) for did, info := range c.deviceListCache { result[did] = info } return result } // ============================================================================ // H4: SubDeviceState / UnsubDeviceState // ============================================================================ // deviceStateSub holds a device state subscription handler. type deviceStateSub struct { handler func(string, MIoTDeviceState, interface{}) ctx interface{} } // SubDeviceState subscribes to device state changes for a given did. // handler is called with (did, state, ctx) when the device state changes. // Aligns with Python: sub_device_state(did, handler, handler_ctx). func (c *MIoTClient) SubDeviceState(did string, handler func(string, MIoTDeviceState, interface{})) error { if did == "" || handler == nil { return NewMIoTClientError("SubDeviceState: did and handler are required") } c.mu.Lock() defer c.mu.Unlock() c.deviceStateSubs[did] = &deviceStateSub{handler: handler, ctx: nil} return nil } // UnsubDeviceState removes a device state subscription for the given did. // Aligns with Python: unsub_device_state(did). func (c *MIoTClient) UnsubDeviceState(did string) error { c.mu.Lock() defer c.mu.Unlock() if _, ok := c.deviceStateSubs[did]; !ok { return NewMIoTClientError("UnsubDeviceState: no subscription for " + did) } delete(c.deviceStateSubs, did) return nil } // SetHomeIDs sets the home IDs for device refresh scope. nil/empty = all homes. func (c *MIoTClient) SetHomeIDs(homeIDs []string) { c.homeIDs = homeIDs } // RefreshAuthInfo 手动触发 token 刷新并同步返回结果(对齐 Python refresh_oauth_info_async)。 // 返回 map 包含 access_token / refresh_token / expires_ts。内部有互斥锁,可安全并发调用。 func (c *MIoTClient) RefreshAuthInfo() (map[string]interface{}, error) { c.refreshMu.Lock() defer c.refreshMu.Unlock() if c.oauth == nil || c.storage == nil { return nil, fmt.Errorf("miot: oauth or storage not initialized") } authInfo := c.storage.LoadUserConfig(c.uid, c.cloudServer, nil) refreshToken, _ := authInfo["refresh_token"].(string) accessToken, _ := authInfo["access_token"].(string) expiresTS, _ := authInfo["expires_ts"].(float64) // 未临近过期且 token 有效 → 直接返回,避免重复刷新浪费 refresh_token now := float64(time.Now().UnixMilli()) / 1000 if accessToken != "" && (expiresTS-now) > float64(MIHOME_REFRESH_THRESHOLD) { return map[string]interface{}{ "access_token": accessToken, "refresh_token": refreshToken, "expires_ts": expiresTS, }, nil } if refreshToken == "" { refreshToken, _ = c.entryData["refresh_token"].(string) } if refreshToken == "" { return nil, fmt.Errorf("miot: no refresh token available") } result, err := c.oauth.RefreshAccessToken(refreshToken) if err != nil { return nil, fmt.Errorf("miot: refresh access token: %w", err) } // Save new auth info c.entryData["access_token"] = result["access_token"] c.entryData["refresh_token"] = result["refresh_token"] if et, ok := result["expires_ts"]; ok { c.entryData["expires_ts"] = et } c.storage.UpdateUserConfig(c.uid, c.cloudServer, map[string]interface{}{ "access_token": result["access_token"], "refresh_token": result["refresh_token"], "expires_ts": result["expires_ts"], }, true) // Update live clients if c.http != nil { if ac, ok := result["access_token"].(string); ok { c.http.UpdateHTTPHeader(c.cloudServer, OAUTH2_CLIENT_ID, ac) } } if c.mipsCloud != nil { ac, _ := result["access_token"].(string) c.mipsCloud.UpdateAccessToken(ac) } c.lgr.Infof("[MIoTClient] RefreshAuthInfo: token refreshed\n") return result, nil } // RequestRefreshUserCert schedules a user certificate refresh after delaySec seconds. // Uses MIHOME_CERT_EXPIRE_MARGIN to determine when refresh is needed. // Aligns with Python: __request_refresh_user_cert(delay_sec). func (c *MIoTClient) RequestRefreshUserCert(delaySec int) { if c.cert == nil || c.http == nil { return } d := time.Duration(delaySec) * time.Second c.mu.Lock() if c.refreshCertTimer != nil { c.refreshCertTimer.Stop() } c.refreshCertTimer = time.AfterFunc(d, func() { c.refreshUserCert() }) c.mu.Unlock() } // refreshUserCert performs the actual cert refresh logic. // Aligns with Python: refresh_user_cert_async. func (c *MIoTClient) refreshUserCert() { if c.cert == nil || c.http == nil { return } // Verify CA cert if ok, err := c.cert.VerifyCACert(); err != nil || !ok { c.lgr.Errorf("[MIoTClient] CA cert verification FAILED: %v\n", err) } // Check remaining time remainSec := c.cert.UserCertRemainingTime() if remainSec > int64(MIHOME_CERT_EXPIRE_MARGIN) { // Cert still valid, schedule next check nextDelay := int(remainSec) - MIHOME_CERT_EXPIRE_MARGIN if nextDelay < 60 { nextDelay = 60 } c.RequestRefreshUserCert(nextDelay) c.lgr.Infof("[MIoTClient] Cert still valid for %ds, recheck in %ds\n", remainSec, nextDelay) return } // Load user key _, err := c.cert.LoadUserKey() if err != nil { c.lgr.Errorf("[MIoTClient] Cert refresh: load key FAILED: %v\n", err) // Retry after delay c.RequestRefreshUserCert(3600) return } // Generate CSR csr, err := c.cert.GenUserCSR() if err != nil { c.lgr.Errorf("[MIoTClient] Cert refresh: gen CSR FAILED: %v\n", err) c.RequestRefreshUserCert(3600) return } // Request cert from cloud certPEM, err := c.http.GetCentralCert(csr) if err != nil { c.lgr.Errorf("[MIoTClient] Cert refresh: get cert from cloud FAILED: %v\n", err) c.RequestRefreshUserCert(RefreshCertRetryDelay) return } // Save cert if err := c.cert.SaveUserCert(certPEM); err != nil { c.lgr.Errorf("[MIoTClient] Cert refresh: save cert FAILED: %v\n", err) return } c.lgr.Infof("[MIoTClient] Cert refreshed successfully\n") // Update MIPS local clients with new certs (if any are still connected) for _, mips := range c.mipsLocal { if mips != nil && mips.IsConnected() { mips.Disconnect() mips.Connect() } } // Schedule next refresh nextDelay := 86400 // 24h default c.RequestRefreshUserCert(nextDelay) } // ============================================================================ // M3: Notification methods // ============================================================================ // ShowClientErrorNotify displays a client error notification via PersistentNotify. // Aligns with Python: __show_client_error_notify(title, message). func (c *MIoTClient) ShowClientErrorNotify(title, message string) { if c.PersistentNotify != nil { c.PersistentNotify("client_error", title, message) } } // ShowDevicesChangedNotify displays a device change notification. // Aligns with Python: __show_devices_changed_notify(). func (c *MIoTClient) ShowDevicesChangedNotify() { if len(c.DisplayDevicesChangedNotify) == 0 { return } if c.PersistentNotify != nil { c.PersistentNotify("devices_changed", "Devices Changed", "Device list has been updated") } } // ShowCentralStateChangedNotify displays a central gateway connection status notification. // Aligns with Python: __show_central_state_changed_notify(connected). func (c *MIoTClient) ShowCentralStateChangedNotify(connected bool) { if c.PersistentNotify != nil { state := "disconnected" if connected { state = "connected" } c.PersistentNotify("central_state", "Central Gateway", state) } } // ============================================================================ // Event handler stubs (called by MIPS/Network/LAN subsystems) // ============================================================================ // onMipsCloudStateChanged handles MIPS cloud connection state changes. // Aligns with Python: __on_mips_cloud_state_changed. func (c *MIoTClient) onMipsCloudStateChanged(key string, connected bool) { if connected { c.lgr.Infof("[MIoTClient] MIPS cloud connected\n") // Refresh devices on reconnect c.ScheduleRefreshDevices(RefreshCloudDevicesDelay * time.Millisecond) return } c.lgr.Infof("[MIoTClient] MIPS cloud disconnected\n") // Disconnect: mark all cloud devices offline c.deviceListMu.Lock() changed := false for did, dev := range c.deviceListCloud { if online, ok := dev["online"].(bool); ok && online { dev["online"] = false c.deviceListCloud[did] = dev changed = true } // Aggregate and update cache c.aggregateDeviceState(did) } c.deviceListMu.Unlock() // Notify state subscribers (offline) if changed { c.deviceListMu.RLock() for did := range c.deviceListCloud { if handler, ok := c.subDeviceState[did]; ok { go handler.Handler(did, map[string]interface{}{"online": false}) } } c.deviceListMu.RUnlock() c.ShowDevicesChangedNotify() } } // onNetworkStatusChanged handles network connectivity changes. // Aligns with Python: __on_network_status_changed. func (c *MIoTClient) onNetworkStatusChanged(status bool) { c.lgr.Infof("[MIoTClient] Network status changed: online=%v\n", status) if status { // Network is back; trigger device refresh and reconnect MIPS if c.mipsCloud != nil && !c.mipsCloud.IsConnected() { c.mipsCloud.Connect() } c.ScheduleRefreshDevices(RefreshCloudDevicesDelay * time.Millisecond) } } // onMipsServiceStateChange handles mDNS service discoveries for central gateways. // Aligns with Python: __on_mips_service_state_change. func (c *MIoTClient) onMipsServiceStateChange(groupID string, state MipsServiceState, data map[string]interface{}) { c.lgr.Infof("[MIoTClient] MIPS service change: group=%s, state=%s\n", groupID, state) virtualDID := getVirtualDID(c.entryData) if virtualDID == "" { c.lgr.Infof("[MIoTClient] MIPS service: missing virtual_did, skip\n") return } if c.cert == nil { c.lgr.Infof("[MIoTClient] MIPS service: cert not initialized, skip\n") return } switch state { case MipsServiceAdded: // Create new MipsLocalClient host, _ := data["addresses"].([]interface{}) if len(host) == 0 { c.lgr.Infof("[MIoTClient] MIPS service: no addresses for %s\n", groupID) return } hostStr, _ := host[0].(string) port, _ := data["port"].(float64) if port == 0 { port = 54321 } mips := NewMipsLocalClient( virtualDID, hostStr, int(port), c.cert.CAFile(), c.cert.CertFile(), c.cert.KeyFile(), ) mips.GroupID = groupID mips.OnDevListChanged = c.onGWDeviceListChanged mips.SubState(groupID, c.onMipsLocalStateChanged) mips.Connect() c.mipsLocal[groupID] = mips c.lgr.Infof("[MIoTClient] MIPS local client created: %s @ %s:%d\n", groupID, hostStr, int(port)) case MipsServiceUpdated: // Disconnect old, connect new if old, exists := c.mipsLocal[groupID]; exists { old.Disconnect() delete(c.mipsLocal, groupID) } host, _ := data["addresses"].([]interface{}) if len(host) == 0 { return } hostStr, _ := host[0].(string) port, _ := data["port"].(float64) if port == 0 { port = 54321 } mips := NewMipsLocalClient( virtualDID, hostStr, int(port), c.cert.CAFile(), c.cert.CertFile(), c.cert.KeyFile(), ) mips.GroupID = groupID mips.OnDevListChanged = c.onGWDeviceListChanged mips.SubState(groupID, c.onMipsLocalStateChanged) mips.Connect() c.mipsLocal[groupID] = mips c.lgr.Infof("[MIoTClient] MIPS local client updated: %s @ %s:%d\n", groupID, hostStr, int(port)) case MipsServiceRemoved: if mips, exists := c.mipsLocal[groupID]; exists { mips.OnDevListChanged = nil mips.UnsubState(groupID) mips.Disconnect() delete(c.mipsLocal, groupID) c.lgr.Infof("[MIoTClient] MIPS local client removed: %s\n", groupID) } } } // onMipsLocalStateChanged handles local MIPS client connection state changes. // Aligns with Python: __on_mips_local_state_changed. // key is the groupID of the gateway. func (c *MIoTClient) onMipsLocalStateChanged(key string, connected bool) { c.lgr.Infof("[MIoTClient] MIPS local state changed: group=%s, connected=%v\n", key, connected) if connected { // Reconnect: pull gateway device list if mips, ok := c.mipsLocal[key]; ok && mips.OnDevListChanged != nil { go mips.OnDevListChanged() } return } // Disconnect: mark gateway devices in this group offline c.deviceListMu.Lock() changed := false for did, dev := range c.deviceListGateway { gid, _ := dev["group_id"].(string) if gid != key { continue } if online, ok := dev["online"].(bool); ok && online { dev["online"] = false dev["push_available"] = false c.deviceListGateway[did] = dev changed = true } // Aggregate and update cache c.aggregateDeviceState(did) } c.deviceListMu.Unlock() if changed { c.deviceListMu.RLock() for did, dev := range c.deviceListGateway { gid, _ := dev["group_id"].(string) if gid == key { if handler, ok := c.subDeviceState[did]; ok { go handler.Handler(did, map[string]interface{}{"online": false}) } } } c.deviceListMu.RUnlock() c.ShowDevicesChangedNotify() } } // onMiotLanStateChange handles LAN controller state transitions. // Aligns with Python: __on_miot_lan_state_change. func (c *MIoTClient) onMiotLanStateChange(initialized bool) { c.lgr.Infof("[MIoTClient] LAN state changed: initialized=%v\n", initialized) if initialized { c.deviceListMu.Lock() lan, ok := c.miotLan.(*MIoTLan) c.deviceListMu.Unlock() if ok { lanDevices := lan.GetDevList() c.deviceListMu.Lock() for did, state := range lanDevices { c.deviceListLan[did] = state // Merge into cache if dev, exists := c.deviceListCache[did]; exists { for k, v := range state { dev[k] = v } } } c.deviceListMu.Unlock() } } } // onLanDeviceStateChanged handles individual LAN device state changes. // Aligns with Python: __on_lan_device_state_changed(did, state, ctx). func (c *MIoTClient) onLanDeviceStateChanged(did string, state map[string]interface{}, ctx interface{}) { c.deviceListMu.Lock() // Update deviceListLan if lanDev, exists := c.deviceListLan[did]; exists { if online, ok := state["online"].(bool); ok { lanDev["online"] = online } if push, ok := state["push_available"].(bool); ok { lanDev["push_available"] = push } c.deviceListLan[did] = lanDev } else { c.deviceListLan[did] = state } // Aggregate three sources c.aggregateDeviceState(did) // Update deviceListCache if cacheInfo, ok := c.deviceListCache[did]; ok { cacheInfo["online"] = cacheInfo["online"] } c.deviceListMu.Unlock() // Notify subDeviceState subscribers if handler, ok := c.subDeviceState[did]; ok { go handler.Handler(did, state) } } // onGWDeviceListChanged handles gateway device list changes. // Aligns with Python: __on_gw_device_list_changed. // Called when a MipsLocalClient reports that its device list has changed. func (c *MIoTClient) onGWDeviceListChanged() { c.lgr.Infof("[MIoTClient] GW device list changed\n") for groupID, mips := range c.mipsLocal { if mips == nil || !mips.IsConnected() { continue } // Pull device list from gateway payload, err := mips.GetDevList("", MipsRequestTimeoutDefault*time.Millisecond) if err != nil { c.lgr.Errorf("[MIoTClient] GW device list pull failed for %s: %v\n", groupID, err) continue } // Parse results: extract DIDs var result struct { Result map[string]interface{} `json:"result"` } if err := json.Unmarshal([]byte(payload), &result); err != nil { c.lgr.Errorf("[MIoTClient] GW device list parse failed for %s: %v\n", groupID, err) continue } // Update gateway device list c.updateDevicesFromGW(groupID, result.Result) } } // updateDevicesFromGW updates deviceListGateway with data from a gateway. func (c *MIoTClient) updateDevicesFromGW(groupID string, devices map[string]interface{}) { c.deviceListMu.Lock() defer c.deviceListMu.Unlock() changed := false for did, di := range devices { info, ok := di.(map[string]interface{}) if !ok { continue } info["group_id"] = groupID info["online"] = true info["push_available"] = true // Merge into gateway list if existing, exists := c.deviceListGateway[did]; exists { for k, v := range info { existing[k] = v } } else { c.deviceListGateway[did] = info changed = true } // Merge into cache if cacheInfo, exists := c.deviceListCache[did]; exists { for k, v := range info { cacheInfo[k] = v } } else { c.deviceListCache[did] = cloneMap(info) } } if changed { c.lgr.Infof("[MIoTClient] GW devices updated: group=%s, count=%d\n", groupID, len(devices)) } } // ============================================================================ // Helper functions // ============================================================================ // buildCloudBroker constructs the MQTT broker URL from cloud server region. func buildCloudBroker(cloudServer string) string { if cloudServer == "cn" || cloudServer == "" { return "ssl://" + DEFAULT_CLOUD_BROKER_HOST + ":8883" } return "ssl://" + cloudServer + "-" + DEFAULT_CLOUD_BROKER_HOST + ":8883" } // getVirtualDID extracts the virtual DID from entry data. func getVirtualDID(entryData map[string]interface{}) string { if entryData == nil { return "" } did, _ := entryData["virtual_did"].(string) return did } // containsStr checks if a string slice contains the given value. func containsStr(slice []string, val string) bool { for _, s := range slice { if s == val { return true } } return false }