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
+43 -19
View File
@@ -25,7 +25,6 @@ const (
mipsQoS = 2
mipsReconnectIntervalMin = 10.0 // seconds
mipsReconnectIntervalMax = 600.0 // seconds
mipsSubPatch = 300
mipsSubInterval = 1 // second
mipsMQTTInterval = 1 // second
uint32max = 0xFFFFFFFF
@@ -269,6 +268,7 @@ type MQTTClient interface {
Disconnect()
IsConnected() bool
Subscribe(topic string, qos byte, handler func(topic string, payload []byte)) error
SubscribeMany(topics map[string]byte) error
Unsubscribe(topic string) error
Publish(topic string, qos byte, payload []byte) error
SetCredentials(username, password string)
@@ -528,34 +528,42 @@ func (c *mipsClient) flushPendingSubs() {
return
}
// 收集所有待订阅 topic,一次 SUBSCRIBE 包批量订阅(大幅减少网络往返)
c.pendingSubMu.Lock()
subbed := 0
batch := make(map[string]byte, len(c.pendingSubs))
for topic, count := range c.pendingSubs {
if subbed >= mipsSubPatch {
break
}
if count > 3 {
delete(c.pendingSubs, topic)
c.lgr.Debugf("[mips] retry sub exceeded: %s", topic)
continue
}
subbed++
if err := c.mqttConn.Subscribe(topic, byte(mipsQoS), func(t string, p []byte) {
c.onMQTTMessage(t, p)
}); err == nil {
delete(c.pendingSubs, topic)
c.lgr.Debugf("[mips] sub success: %s", topic)
} else {
c.pendingSubs[topic] = count + 1
c.lgr.Debugf("[mips] retry sub %d: %s %v", count, topic, err)
}
batch[topic] = byte(mipsQoS)
}
hasMore := len(c.pendingSubs) > 0
c.pendingSubMu.Unlock()
if hasMore {
if len(batch) == 0 {
return
}
if err := c.mqttConn.SubscribeMany(batch); err == nil {
c.pendingSubMu.Lock()
for t := range batch {
delete(c.pendingSubs, t)
c.lgr.Debugf("[mips] sub success: %s", t)
}
hasMore := len(c.pendingSubs) > 0
c.pendingSubMu.Unlock()
if hasMore {
time.Sleep(time.Duration(mipsSubInterval) * time.Second)
c.flushPendingSubs()
}
} else {
c.lgr.Debugf("[mips] batch sub error: %v", err)
c.pendingSubMu.Lock()
for t := range batch {
c.pendingSubs[t] = c.pendingSubs[t] + 1
}
c.pendingSubMu.Unlock()
time.Sleep(time.Duration(mipsSubInterval) * time.Second)
c.flushPendingSubs()
}
@@ -1042,6 +1050,22 @@ func (p *pahoMQTTClient) Subscribe(topic string, qos byte, handler func(topic st
return nil
}
// SubscribeMany 用单个 SUBSCRIBE 包批量订阅多个 topic,显著减少网络往返。
// 消息分发仍走 DefaultPublishHandler,与 Subscribe 行为一致。
func (p *pahoMQTTClient) SubscribeMany(topics map[string]byte) error {
if p.client == nil || !p.client.IsConnected() {
return fmt.Errorf("not connected")
}
if len(topics) == 0 {
return nil
}
token := p.client.SubscribeMultiple(topics, nil)
if token.Wait() && token.Error() != nil {
return token.Error()
}
return nil
}
func (p *pahoMQTTClient) Unsubscribe(topic string) error {
if p.client == nil || !p.client.IsConnected() {
return fmt.Errorf("not connected")