按照 TOKEN_REFACTOR.md / FIX_CONFIG.md 方案重构核心层: - miot: Init() 优先读 storage token,新增 RefreshAuthInfo() 和刷新回调 - xiaomi: Config 改用 AuthInfo 结构体(含 UUID/UID/DataDir/OnTokenRefreshed) - xiaomi: NewClient(ctx, Config) 新签名,新增 RefreshToken(),后台自动刷新 - xiaomi/devices: 设备控制接口重构 - 新增示例 12_ac_subscribe,其余示例适配新 Config
1235 lines
37 KiB
Go
1235 lines
37 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
|
|
refreshMu sync.Mutex // token 刷新互斥锁
|
|
|
|
// === Callbacks ===
|
|
tokenRefreshCB func(accessToken, refreshToken string)
|
|
}
|
|
|
|
// 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 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
|
|
}
|
|
fmt.Printf("[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)
|
|
fmt.Printf("[MIoTClient] Saved initial token to storage\n")
|
|
}
|
|
}
|
|
|
|
// Keep access_token in entryData for downstream use
|
|
c.entryData["access_token"] = accessToken
|
|
|
|
// 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 (use token from storage if available)
|
|
if c.http == nil && 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 (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 {
|
|
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
|
|
}
|
|
|
|
// ============================================================================
|
|
// SetTokenRefreshCallback sets a callback invoked when the background timer refreshes the token.
|
|
func (c *MIoTClient) SetTokenRefreshCallback(cb func(accessToken, refreshToken string)) {
|
|
c.tokenRefreshCB = cb
|
|
}
|
|
|
|
// 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() {
|
|
c.refreshMu.Lock()
|
|
defer c.refreshMu.Unlock()
|
|
|
|
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_REFRESH_THRESHOLD
|
|
now := float64(time.Now().UnixMilli()) / 1000
|
|
needRefresh := (expiresTS - now) <= float64(MIHOME_REFRESH_THRESHOLD)
|
|
|
|
if !needRefresh && accessToken != "" {
|
|
// Schedule next check
|
|
nextDelay := int(expiresTS - now - float64(MIHOME_REFRESH_THRESHOLD))
|
|
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)
|
|
var newExpiresTS float64
|
|
switch v := result["expires_ts"].(type) {
|
|
case float64: newExpiresTS = v
|
|
case int64: newExpiresTS = float64(v)
|
|
}
|
|
|
|
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")
|
|
|
|
// Notify callback (xiaomi layer uses this to persist to DB)
|
|
if c.tokenRefreshCB != nil {
|
|
c.tokenRefreshCB(newAccessToken, newRefreshToken)
|
|
}
|
|
|
|
// Schedule next refresh
|
|
nextDelay := int(newExpiresTS - now - float64(MIHOME_REFRESH_THRESHOLD))
|
|
if nextDelay < 60 {
|
|
nextDelay = 60
|
|
}
|
|
c.RequestRefreshAuthInfo(nextDelay)
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
|
|
fmt.Printf("[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 {
|
|
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
|
|
}
|