Files
xiaomihome/miot/miot_client.go
T
4566704 3a44cb9e6a feat: 初始化小米 IoT (MIoT) 智能家居 Go 库
- 实现 MIoT 客户端核心功能(MQTT 连接、设备管理、属性读写)
- 支持云端 API 调用与局域网设备发现(mDNS)
- 集成国际化(i18n)多语言支持
- 添加 MIoT 设备规约解析器(spec_parser)
- 包含单元测试与使用示例
2026-06-28 22:05:49 +08:00

1129 lines
33 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 (
"context"
"encoding/json"
"fmt"
"sync"
"time"
)
// ============================================================================
// 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
// === 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) ===
refreshTokenTimer *time.Timer
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
}
// 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())
return &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,
}
}
// Init initializes the MIoTClient.
// Aligns with Python MIoTClient.init_async.
func (c *MIoTClient) Init() error {
// 1. Load user config
if c.storage != nil {
config := c.storage.LoadUserConfig(c.uid, c.cloudServer, nil)
if len(config) > 0 {
fmt.Printf("[MIoTClient] Loaded user config\n")
}
_ = config
}
// 2. Load device cache
if err := c.loadCacheDevice(); err != nil {
fmt.Printf("[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)
fmt.Printf("[MIoTClient] OAuth client initialized\n")
}
// 3.2 Initialize Cert client
cert, err := NewMIoTCert(c.storage, c.uid, c.cloudServer)
if err != nil {
fmt.Printf("[MIoTClient] Warning: cert init failed: %v\n", err)
} else {
c.cert = cert
c.cert.VerifyCACert()
fmt.Printf("[MIoTClient] Cert client initialized\n")
}
// 4. Initialize HTTP client (from existing code path)
if c.http == nil && c.entryData != nil {
accessToken, _ := c.entryData["access_token"].(string)
if accessToken != "" {
httpClient, err := NewMIoTHttpClient(c.cloudServer, OAUTH2_CLIENT_ID, accessToken)
if err != nil {
fmt.Printf("[MIoTClient] Warning: failed to create HTTP client: %v\n", err)
} else {
c.http = httpClient
fmt.Printf("[MIoTClient] HTTP client created\n")
}
}
}
// 3.3 Initialize MipsCloudClient
if c.mipsCloud == nil && c.entryData != nil {
accessToken, _ := c.entryData["access_token"].(string)
uuid, _ := c.entryData["uuid"].(string)
if accessToken != "" && 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 {
fmt.Printf("[MIoTClient] Warning: MIPS cloud connect failed: %v\n", err)
} else {
fmt.Printf("[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)
fmt.Printf("[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)
fmt.Printf("[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() {
fmt.Printf("[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)
}
fmt.Printf("[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()
fmt.Printf("[MIoTClient] i18n initialized (lang=%s)\n", lang)
fmt.Printf("[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()
fmt.Printf("[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 token/cert timers
if c.refreshTokenTimer != nil {
c.refreshTokenTimer.Stop()
c.refreshTokenTimer = nil
}
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 {
fmt.Printf("[MIoTClient] Warning: failed to save devices on deinit: %v\n", err)
}
fmt.Printf("[MIoTClient] Device cache saved to storage\n")
}
// Cancel context
if c.cancel != nil {
c.cancel()
}
// Clear subscription tree
c.subTree.Clear()
fmt.Printf("[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
}
}
fmt.Printf("[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
}
// ============================================================================
// M2: RequestRefreshAuthInfo / RequestRefreshUserCert
// ============================================================================
// RequestRefreshAuthInfo schedules an OAuth token refresh after delaySec seconds.
// Uses MIHOME_CERT_EXPIRE_MARGIN to determine when refresh is needed.
// Aligns with Python: __request_refresh_auth_info(delay_sec).
func (c *MIoTClient) RequestRefreshAuthInfo(delaySec int) {
if c.oauth == nil || c.storage == nil {
return
}
d := time.Duration(delaySec) * time.Second
c.mu.Lock()
if c.refreshTokenTimer != nil {
c.refreshTokenTimer.Stop()
}
c.refreshTokenTimer = time.AfterFunc(d, func() {
c.refreshAuthInfo()
})
c.mu.Unlock()
}
// refreshAuthInfo performs the actual token refresh logic.
// Aligns with Python: refresh_oauth_info_async.
func (c *MIoTClient) refreshAuthInfo() {
if c.oauth == nil || c.storage == nil {
return
}
// Load current auth config
authInfo := c.storage.LoadUserConfig(c.uid, c.cloudServer, nil)
accessToken, _ := authInfo["access_token"].(string)
refreshToken, _ := authInfo["refresh_token"].(string)
expiresTS, _ := authInfo["expires_ts"].(float64)
// Check if refresh is needed: expires_ts - now <= MIHOME_CERT_EXPIRE_MARGIN
now := float64(time.Now().UnixMilli()) / 1000
needRefresh := (expiresTS - now) <= float64(MIHOME_CERT_EXPIRE_MARGIN)
if !needRefresh && accessToken != "" {
// Schedule next check
nextDelay := int(expiresTS - now - float64(MIHOME_CERT_EXPIRE_MARGIN))
if nextDelay < 60 {
nextDelay = 60
}
c.RequestRefreshAuthInfo(nextDelay)
fmt.Printf("[MIoTClient] Token still valid, recheck in %ds\n", nextDelay)
return
}
if refreshToken == "" && accessToken == "" {
fmt.Printf("[MIoTClient] No refresh token available\n")
return
}
// Refresh access token
result, err := c.oauth.RefreshAccessToken(refreshToken)
if err != nil {
fmt.Printf("[MIoTClient] Token refresh FAILED: %v\n", err)
// Retry after delay
c.RequestRefreshAuthInfo(RefreshAuthRetryDelay)
return
}
newAccessToken, _ := result["access_token"].(string)
newRefreshToken, _ := result["refresh_token"].(string)
newExpiresTS, _ := result["expires_ts"].(float64)
if newAccessToken == "" {
fmt.Printf("[MIoTClient] Token refresh returned empty access_token\n")
return
}
// Update HTTP client
if c.http != nil {
c.http.UpdateHTTPHeader(c.cloudServer, OAUTH2_CLIENT_ID, newAccessToken)
}
// Update MIPS cloud client
if c.mipsCloud != nil {
c.mipsCloud.UpdateAccessToken(newAccessToken)
}
// Save new auth info
newAuthInfo := map[string]interface{}{
"access_token": newAccessToken,
"refresh_token": newRefreshToken,
"expires_ts": newExpiresTS,
}
c.storage.UpdateUserConfig(c.uid, c.cloudServer, newAuthInfo, true)
fmt.Printf("[MIoTClient] Token refreshed successfully\n")
// Schedule next refresh
nextDelay := int(newExpiresTS - now - float64(MIHOME_CERT_EXPIRE_MARGIN))
if nextDelay < 60 {
nextDelay = 60
}
c.RequestRefreshAuthInfo(nextDelay)
}
// 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 {
fmt.Printf("[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)
fmt.Printf("[MIoTClient] Cert still valid for %ds, recheck in %ds\n", remainSec, nextDelay)
return
}
// Load user key
_, err := c.cert.LoadUserKey()
if err != nil {
fmt.Printf("[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 {
fmt.Printf("[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 {
fmt.Printf("[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 {
fmt.Printf("[MIoTClient] Cert refresh: save cert FAILED: %v\n", err)
return
}
fmt.Printf("[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 {
fmt.Printf("[MIoTClient] MIPS cloud connected\n")
// Refresh devices on reconnect
c.ScheduleRefreshDevices(RefreshCloudDevicesDelay * time.Millisecond)
return
}
fmt.Printf("[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) {
fmt.Printf("[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{}) {
fmt.Printf("[MIoTClient] MIPS service change: group=%s, state=%s\n", groupID, state)
virtualDID := getVirtualDID(c.entryData)
if virtualDID == "" {
fmt.Printf("[MIoTClient] MIPS service: missing virtual_did, skip\n")
return
}
if c.cert == nil {
fmt.Printf("[MIoTClient] MIPS service: cert not initialized, skip\n")
return
}
switch state {
case MipsServiceAdded:
// Create new MipsLocalClient
host, _ := data["addresses"].([]interface{})
if len(host) == 0 {
fmt.Printf("[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
fmt.Printf("[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
fmt.Printf("[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)
fmt.Printf("[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) {
fmt.Printf("[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) {
fmt.Printf("[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() {
fmt.Printf("[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 {
fmt.Printf("[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 {
fmt.Printf("[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 {
fmt.Printf("[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
}