feat: AC 三合一设备支持 + 新增 Plug/Gateway 驱动 + Cover 结构化重构

**AC 驱动增强**
- 支持 thermostat + air-conditioner + air-fresh 多服务三合一设备,属性跨服务合并解析
- 新增 specResolver.findValListInService/findInService/hasService/serviceShortName 方法
- 移除 KindThermostat,heater/electric-blanket/thermostat 重新归类为 KindAirConditioner

**Cover 窗帘重构**
- CoverProps 结构化:Position/Status/Fault 分离
- SetPosition 优先写 target-position,回退 current-position
- OnPropsChanged/FetchProps 支持多属性同步刷新

**新增设备类型**
- Plug 驱动:插座/插头,支持开合、在线状态、属性变更订阅
- Gateway 驱动:网关设备类型识别
- miot/miot_errors.go:新增 MIoT 云端错误码定义

**MIoT 客户端健壮性**
- SetProp 空结果视为失败;设备被移除或离线时自动刷新设备列表
- GetDevices 的 has_more 改用 parseBool 严格解析
- MIPS SubscribeMany 批量订阅减少往返
- matchURN 改为按段精确匹配,避免 tofan 误匹配 fan

**Bridge**
- 移除 Thermostat case,Cover 改用结构化 CoverProps
- 新增设备创建后自动订阅属性变更

**其他**
- 日志整体 Info → Debug 降噪
- .gitignore 忽略 /.codebuddy/memory
- 更新 devices_test 与 08_adv_filter_classify 示例
This commit is contained in:
2026-08-23 19:42:55 +08:00
parent 04333750ab
commit 86c21ce91c
25 changed files with 1129 additions and 155 deletions
+30 -30
View File
@@ -156,7 +156,7 @@ func (c *MIoTClient) Init() error {
if et, ok := authInfo["expires_ts"].(float64); ok {
c.entryData["expires_ts"] = et
}
c.lgr.Infof("[MIoTClient] Loaded token from storage\n")
c.lgr.Debugf("[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
@@ -168,7 +168,7 @@ func (c *MIoTClient) Init() error {
"refresh_token": refreshToken,
"expires_ts": expiresTS,
}, true)
c.lgr.Infof("[MIoTClient] Saved initial token to storage\n")
c.lgr.Debugf("[MIoTClient] Saved initial token to storage\n")
}
}
@@ -187,7 +187,7 @@ func (c *MIoTClient) Init() error {
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")
c.lgr.Debugf("[MIoTClient] OAuth client initialized\n")
}
// 3.2 Initialize Cert client
@@ -197,7 +197,7 @@ func (c *MIoTClient) Init() error {
} else {
c.cert = cert
c.cert.VerifyCACert()
c.lgr.Infof("[MIoTClient] Cert client initialized\n")
c.lgr.Debugf("[MIoTClient] Cert client initialized\n")
}
// 4. Initialize HTTP client (use token from storage if available)
@@ -207,7 +207,7 @@ func (c *MIoTClient) Init() error {
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")
c.lgr.Debugf("[MIoTClient] HTTP client created\n")
}
}
@@ -225,7 +225,7 @@ func (c *MIoTClient) Init() error {
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.lgr.Debugf("[MIoTClient] MIPS cloud connected\n")
}
c.mu.Lock()
}
@@ -239,7 +239,7 @@ func (c *MIoTClient) Init() error {
nw.SubNetworkStatus(key, c.onNetworkStatusChanged)
status := nw.GetNetworkStatus()
c.onNetworkStatusChanged(status)
c.lgr.Infof("[MIoTClient] Network status subscribed (online=%v)\n", status)
c.lgr.Debugf("[MIoTClient] Network status subscribed (online=%v)\n", status)
}
}
@@ -258,12 +258,12 @@ func (c *MIoTClient) Init() error {
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)
c.lgr.Debugf("[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)
c.lgr.Debugf("[MIoTClient] Central mips service scanned: %s, %v\n", homeID, sd)
mips := NewMipsLocalClient(
getVirtualDID(c.entryData),
sd.Addresses[0],
@@ -294,7 +294,7 @@ func (c *MIoTClient) Init() error {
if lan.InitDone() {
c.onMiotLanStateChange(true)
}
c.lgr.Infof("[MIoTClient] LAN control initialized\n")
c.lgr.Debugf("[MIoTClient] LAN control initialized\n")
}
}
} else {
@@ -324,7 +324,7 @@ func (c *MIoTClient) Init() error {
}
c.i18n = NewMIoTI18n(lang)
c.i18n.Init()
c.lgr.Infof("[MIoTClient] i18n initialized (lang=%s)\n", lang)
c.lgr.Debugf("[MIoTClient] i18n initialized (lang=%s)\n", lang)
c.lgr.Infof("[MIoTClient] Initialized\n")
return nil
@@ -358,7 +358,7 @@ func (c *MIoTClient) Deinit() error {
if c.mipsCloud != nil {
c.mipsCloud.UnsubState(key)
c.mipsCloud.Disconnect()
c.lgr.Infof("[MIoTClient] MIPS cloud disconnected\n")
c.lgr.Debugf("[MIoTClient] MIPS cloud disconnected\n")
}
// 4. Cancel refresh cloud devices timer
@@ -493,7 +493,7 @@ func (c *MIoTClient) loadCacheDevice() error {
c.deviceListCache[did] = infoMap
}
}
c.lgr.Infof("[MIoTClient] Loaded %d devices from cache\n", len(deviceList))
c.lgr.Debugf("[MIoTClient] Loaded %d devices from cache\n", len(deviceList))
return nil
}
@@ -647,7 +647,7 @@ func (c *MIoTClient) RefreshAuthInfo() (map[string]interface{}, error) {
c.mipsCloud.UpdateAccessToken(ac)
}
c.lgr.Infof("[MIoTClient] RefreshAuthInfo: token refreshed\n")
c.lgr.Debugf("[MIoTClient] RefreshAuthInfo: token refreshed\n")
return result, nil
}
@@ -690,7 +690,7 @@ func (c *MIoTClient) refreshUserCert() {
nextDelay = 60
}
c.RequestRefreshUserCert(nextDelay)
c.lgr.Infof("[MIoTClient] Cert still valid for %ds, recheck in %ds\n", remainSec, nextDelay)
c.lgr.Debugf("[MIoTClient] Cert still valid for %ds, recheck in %ds\n", remainSec, nextDelay)
return
}
@@ -720,7 +720,7 @@ func (c *MIoTClient) refreshUserCert() {
}
// Save cert
if err := c.cert.SaveUserCert(certPEM); err != nil {
if err = c.cert.SaveUserCert(certPEM); err != nil {
c.lgr.Errorf("[MIoTClient] Cert refresh: save cert FAILED: %v\n", err)
return
}
@@ -783,13 +783,13 @@ func (c *MIoTClient) ShowCentralStateChangedNotify(connected bool) {
// 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")
c.lgr.Debugf("[MIoTClient] MIPS cloud connected\n")
// Refresh devices on reconnect
c.ScheduleRefreshDevices(RefreshCloudDevicesDelay * time.Millisecond)
return
}
c.lgr.Infof("[MIoTClient] MIPS cloud disconnected\n")
c.lgr.Debugf("[MIoTClient] MIPS cloud disconnected\n")
// Disconnect: mark all cloud devices offline
c.deviceListMu.Lock()
@@ -821,7 +821,7 @@ func (c *MIoTClient) onMipsCloudStateChanged(key string, connected bool) {
// 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)
c.lgr.Debugf("[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() {
@@ -834,15 +834,15 @@ func (c *MIoTClient) onNetworkStatusChanged(status bool) {
// 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)
c.lgr.Debugf("[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")
c.lgr.Debugf("[MIoTClient] MIPS service: missing virtual_did, skip\n")
return
}
if c.cert == nil {
c.lgr.Infof("[MIoTClient] MIPS service: cert not initialized, skip\n")
c.lgr.Debugf("[MIoTClient] MIPS service: cert not initialized, skip\n")
return
}
@@ -851,7 +851,7 @@ func (c *MIoTClient) onMipsServiceStateChange(groupID string, state MipsServiceS
// Create new MipsLocalClient
host, _ := data["addresses"].([]interface{})
if len(host) == 0 {
c.lgr.Infof("[MIoTClient] MIPS service: no addresses for %s\n", groupID)
c.lgr.Debugf("[MIoTClient] MIPS service: no addresses for %s\n", groupID)
return
}
hostStr, _ := host[0].(string)
@@ -872,7 +872,7 @@ func (c *MIoTClient) onMipsServiceStateChange(groupID string, state MipsServiceS
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))
c.lgr.Debugf("[MIoTClient] MIPS local client created: %s @ %s:%d\n", groupID, hostStr, int(port))
case MipsServiceUpdated:
// Disconnect old, connect new
@@ -902,7 +902,7 @@ func (c *MIoTClient) onMipsServiceStateChange(groupID string, state MipsServiceS
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))
c.lgr.Debugf("[MIoTClient] MIPS local client updated: %s @ %s:%d\n", groupID, hostStr, int(port))
case MipsServiceRemoved:
if mips, exists := c.mipsLocal[groupID]; exists {
@@ -910,7 +910,7 @@ func (c *MIoTClient) onMipsServiceStateChange(groupID string, state MipsServiceS
mips.UnsubState(groupID)
mips.Disconnect()
delete(c.mipsLocal, groupID)
c.lgr.Infof("[MIoTClient] MIPS local client removed: %s\n", groupID)
c.lgr.Debugf("[MIoTClient] MIPS local client removed: %s\n", groupID)
}
}
}
@@ -919,7 +919,7 @@ func (c *MIoTClient) onMipsServiceStateChange(groupID string, state MipsServiceS
// 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)
c.lgr.Debugf("[MIoTClient] MIPS local state changed: group=%s, connected=%v\n", key, connected)
if connected {
// Reconnect: pull gateway device list
@@ -966,7 +966,7 @@ func (c *MIoTClient) onMipsLocalStateChanged(key string, connected bool) {
// 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)
c.lgr.Debugf("[MIoTClient] LAN state changed: initialized=%v\n", initialized)
if initialized {
c.deviceListMu.Lock()
lan, ok := c.miotLan.(*MIoTLan)
@@ -1026,7 +1026,7 @@ func (c *MIoTClient) onLanDeviceStateChanged(did string, state map[string]interf
// 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")
c.lgr.Debugf("[MIoTClient] GW device list changed\n")
for groupID, mips := range c.mipsLocal {
if mips == nil || !mips.IsConnected() {
@@ -1090,7 +1090,7 @@ func (c *MIoTClient) updateDevicesFromGW(groupID string, devices map[string]inte
}
if changed {
c.lgr.Infof("[MIoTClient] GW devices updated: group=%s, count=%d\n", groupID, len(devices))
c.lgr.Debugf("[MIoTClient] GW devices updated: group=%s, count=%d\n", groupID, len(devices))
}
}