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
+57 -36
View File
@@ -477,7 +477,7 @@ func (c *MIoTHttpClient) getDevRoomPage(maxID string) (map[string]map[string]int
}
// Check for more pages
hasMore, _ := result["has_more"].(bool)
hasMore := parseBool(result["has_more"])
nextMaxID, _ := result["max_id"].(string)
if hasMore && nextMaxID != "" {
nextList, err := c.getDevRoomPage(nextMaxID)
@@ -582,42 +582,43 @@ func (c *MIoTHttpClient) GetHomeInfos() (map[string]interface{}, error) {
}
// Handle pagination
hasMore, _ := result["has_more"].(bool)
hasMore := parseBool(result["has_more"])
maxID, _ := result["max_id"].(string)
if hasMore && maxID != "" {
moreList, err := c.getDevRoomPage(maxID)
if err == nil {
for _, deviceSource := range []string{"homelist", "share_home_list"} {
for hidStr, info := range moreList {
if _, exists := homeInfos[deviceSource][hidStr]; !exists {
continue
}
h, _ := homeInfos[deviceSource][hidStr].(map[string]interface{})
if h == nil {
continue
}
h["dids"] = append(toStringSlice(h["dids"]), toStringSlice(info["dids"])...)
if err != nil {
return nil, err
}
for _, deviceSource := range []string{"homelist", "share_home_list"} {
for hidStr, info := range moreList {
if _, exists := homeInfos[deviceSource][hidStr]; !exists {
continue
}
h, _ := homeInfos[deviceSource][hidStr].(map[string]interface{})
if h == nil {
continue
}
h["dids"] = append(toStringSlice(h["dids"]), toStringSlice(info["dids"])...)
for rid, rinfo := range info["room_info"].(map[string]map[string]interface{}) {
hRoomInfo, _ := h["room_info"].(map[string]interface{})
if hRoomInfo == nil {
hRoomInfo = make(map[string]interface{})
h["room_info"] = hRoomInfo
}
if hRoomInfo[rid] == nil {
hRoomInfo[rid] = map[string]interface{}{
"room_id": rid,
"room_name": "",
"dids": []string{},
}
}
hrEntry, _ := hRoomInfo[rid].(map[string]interface{})
if hrEntry != nil {
hrEntry["dids"] = append(
toStringSlice(hrEntry["dids"]),
toStringSlice(rinfo["dids"])...)
for rid, rinfo := range info["room_info"].(map[string]map[string]interface{}) {
hRoomInfo, _ := h["room_info"].(map[string]interface{})
if hRoomInfo == nil {
hRoomInfo = make(map[string]interface{})
h["room_info"] = hRoomInfo
}
if hRoomInfo[rid] == nil {
hRoomInfo[rid] = map[string]interface{}{
"room_id": rid,
"room_name": "",
"dids": []string{},
}
}
hrEntry, _ := hRoomInfo[rid].(map[string]interface{})
if hrEntry != nil {
hrEntry["dids"] = append(
toStringSlice(hrEntry["dids"]),
toStringSlice(rinfo["dids"])...)
}
}
}
}
@@ -725,13 +726,14 @@ func (c *MIoTHttpClient) getDeviceListPage(dids []string, startDID string) (map[
// Handle pagination
nextStartDID, _ := result["next_start_did"].(string)
hasMore, _ := result["has_more"].(bool)
hasMore := parseBool(result["has_more"])
if hasMore && nextStartDID != "" {
nextInfos, err := c.getDeviceListPage(dids, nextStartDID)
if err == nil {
for k, v := range nextInfos {
deviceInfos[k] = v
}
if err != nil {
return nil, err
}
for k, v := range nextInfos {
deviceInfos[k] = v
}
}
@@ -1196,3 +1198,22 @@ func containsString(slice []string, val string) bool {
}
return false
}
// parseBool 兼容云端多种布尔表达(bool / 0|1 / "true"|"false")。
func parseBool(v interface{}) bool {
switch n := v.(type) {
case bool:
return n
case float64:
return n != 0
case float32:
return n != 0
case int:
return n != 0
case int64:
return n != 0
case string:
return n == "true" || n == "1"
}
return false
}