feat: State→Props 命名统一 + 新增 Plug/Gateway 设备 + 分类系统重构

**命名统一(State → Props)**
- 所有设备类型:XxxState → XxxProps,GetState() → GetProps(),FetchState() → FetchProps()
- 回调接口:OnStateChanged → OnPropsChanged
- Bridge 层类型别名、示例代码、文档同步更名

**新增设备类型**
- KindPlug + Plug 驱动:智能插座/插头(TurnOn/TurnOff/IsOn/OnPropsChanged)
- KindGateway + Gateway 驱动:网关设备类型识别
- KindOccupancySensor 从 KindSensor 独立,添加 OccupancyProps

**设备分类系统改进**
- classifyByModel 改为按 key 长度降序匹配(防止 sensor 先于 sensor-occupy)
- 新增下划线变体识别(sensor_occupy、sensor_temp 等)
- outlet 重新归类为 Plug(非 Switch)
- 新增 Or() 组合过滤器

**连接桥增强**
- 设备创建成功后自动订阅属性变更
- 新增 nil 防护检查
- 支持 Plug/Speaker/Gateway/Occupancy 类型的 FetchProps 刷新
- 日志级别 Info → Debug

**AC 驱动增强**
- 新增 HasPower()、GetElectricPower()、GetPowerConsumption() 功率接口
- ACProps 新增 ElectricPower、PowerConsumption 字段
- valMapper:Desc 为空时 fallback 用 Name,key 统一小写

**MIoT 核心**
- 设备列表刷新改用 reflect.DeepEqual 全字段变更检测
- 移除 BLE/代理设备默认在线 hack
- RefreshDeviceAllProps 增加离线检查
- 新增 miot_errors.go:MIoT 错误码定义
- 全局日志级别 Info→Debug/Warn(降低噪音)

**示例与文档**
- 所有示例代码同步 API 变更
- 传感器示例改用 Or 组合过滤器
- ARCH_PLAN 等迁移文档同步更新
This commit is contained in:
2026-08-05 17:54:08 +08:00
parent b808e51b3e
commit 04333750ab
37 changed files with 916 additions and 536 deletions
+29 -29
View File
@@ -156,7 +156,7 @@ func newLanDevice(mgr *MIoTLan, did, tokenHex, ip string) (*LanDevice, error) {
d.updateKeepAlive(LanDeviceDead)
})
mgr.lgr.Infof("[lan] device added: did=%s", d.DID)
mgr.lgr.Debugf("[lan] device added: did=%s", d.DID)
return d, nil
}
@@ -165,7 +165,7 @@ func (d *LanDevice) KeepAlive(ip, ifName string) {
d.IP = ip
if d.IfName != ifName {
d.IfName = ifName
d.manager.lgr.Infof("[lan] device if_name changed: %s, did=%s", d.IfName, d.DID)
d.manager.lgr.Debugf("[lan] device if_name changed: %s, did=%s", d.IfName, d.DID)
}
d.updateKeepAlive(LanDeviceFresh)
}
@@ -316,12 +316,12 @@ func (d *LanDevice) Subscribe() {
func (d *LanDevice) subscribeHandler(msg map[string]interface{}, subTS int) {
result, _ := msg["result"].(map[string]interface{})
if result == nil {
d.manager.lgr.Infof("[lan] subscribe error: no result, did=%s, msg=%v", d.DID, msg)
d.manager.lgr.Warnf("[lan] subscribe error: no result, did=%s, msg=%v", d.DID, msg)
return
}
code, _ := result["code"].(float64)
if code != 0 {
d.manager.lgr.Infof("[lan] subscribe error: code=%v, did=%s, msg=%v", code, d.DID, msg)
d.manager.lgr.Warnf("[lan] subscribe error: code=%v, did=%s, msg=%v", code, d.DID, msg)
return
}
d.subscribed = true
@@ -330,7 +330,7 @@ func (d *LanDevice) subscribeHandler(msg map[string]interface{}, subTS int) {
"online": d.online,
"push_available": d.subscribed,
})
d.manager.lgr.Infof("[lan] subscribe success: if=%s, did=%s", d.IfName, d.DID)
d.manager.lgr.Debugf("[lan] subscribe success: if=%s, did=%s", d.IfName, d.DID)
}
// Unsubscribe sends a miIO.unsub request to the device.
@@ -351,11 +351,11 @@ func (d *LanDevice) Unsubscribe() {
result, _ := msg["result"].(map[string]interface{})
if result != nil {
if code, _ := result["code"].(float64); code != 0 {
d.manager.lgr.Infof("[lan] unsubscribe error: code=%v, did=%s, msg=%v", code, d.DID, msg)
d.manager.lgr.Warnf("[lan] unsubscribe error: code=%v, did=%s, msg=%v", code, d.DID, msg)
return
}
}
d.manager.lgr.Infof("[lan] unsubscribe success: if=%s, did=%s", d.IfName, d.DID)
d.manager.lgr.Debugf("[lan] unsubscribe success: if=%s, did=%s", d.IfName, d.DID)
}, nil, 5000)
d.subscribed = false
d.manager.broadcastDeviceState(d.DID, map[string]interface{}{
@@ -374,7 +374,7 @@ func (d *LanDevice) OnDelete() {
d.unstableTimer.Stop()
d.unstableTimer = nil
}
d.manager.lgr.Infof("[lan] device deleted: did=%s", d.DID)
d.manager.lgr.Debugf("[lan] device deleted: did=%s", d.DID)
}
// UpdateInfo updates the device token if it has changed.
@@ -396,12 +396,12 @@ func (d *LanDevice) UpdateInfo(info map[string]interface{}) {
aesIV := md5Sum(append(aesKey[:], d.Token...))
block, err := aes.NewCipher(aesKey[:])
if err != nil {
d.manager.lgr.Infof("[lan] update token cipher error: %v, did=%s", err, d.DID)
d.manager.lgr.Warnf("[lan] update token cipher error: %v, did=%s", err, d.DID)
return
}
d.cipher = block
d.aesIV = aesIV[:]
d.manager.lgr.Infof("[lan] token updated: did=%s", d.DID)
d.manager.lgr.Debugf("[lan] token updated: did=%s", d.DID)
}
// ============================================================================
@@ -413,7 +413,7 @@ func (d *LanDevice) updateKeepAlive(state LanDeviceState) {
d.state = state
if d.state != LanDeviceFresh {
d.manager.lgr.Infof("[lan] device status: did=%s, state=%s", d.DID, d.state)
d.manager.lgr.Debugf("[lan] device status: did=%s, state=%s", d.DID, d.state)
}
// Cancel existing timer
@@ -441,11 +441,11 @@ func (d *LanDevice) updateKeepAlive(state LanDeviceState) {
})
// Fast ping
if d.IfName == "" {
d.manager.lgr.Infof("[lan] if_name not set for device: did=%s", d.DID)
d.manager.lgr.Debugf("[lan] if_name not set for device: did=%s", d.DID)
return
}
if d.IP == "" {
d.manager.lgr.Infof("[lan] ip not set for device: did=%s", d.DID)
d.manager.lgr.Debugf("[lan] ip not set for device: did=%s", d.DID)
return
}
d.manager.ping(d.IfName, d.IP)
@@ -464,7 +464,7 @@ func (d *LanDevice) nextKATimeout() float64 {
}
func (d *LanDevice) changeOnline(online bool) {
d.manager.lgr.Infof("[lan] change online: did=%s, online=%v", d.DID, online)
d.manager.lgr.Debugf("[lan] change online: did=%s, online=%v", d.DID, online)
tsNow := int(time.Now().Unix())
d.onlineHistory = append(d.onlineHistory, onlineEvent{ts: tsNow, online: online})
if len(d.onlineHistory) > networkUnstableCntTh {
@@ -483,9 +483,9 @@ func (d *LanDevice) changeOnline(online bool) {
float64(tsNow-d.onlineHistory[0].ts) > networkUnstableTimeTh {
d.setOnline(true)
} else {
d.manager.lgr.Infof("[lan] unstable device detected: did=%s", d.DID)
d.manager.lgr.Debugf("[lan] unstable device detected: did=%s", d.DID)
d.unstableTimer = time.AfterFunc(time.Duration(networkUnstableResumeTh*float64(time.Second)), func() {
d.manager.lgr.Infof("[lan] unstable resume threshold passed: did=%s", d.DID)
d.manager.lgr.Debugf("[lan] unstable resume threshold passed: did=%s", d.DID)
d.setOnline(true)
})
}
@@ -872,7 +872,7 @@ func (m *MIoTLan) createSocketLocked(ifName string) {
conn, err := createBoundSocket(info.IP, m.localPort)
if err != nil {
m.lgr.Infof("[lan] create socket error: if=%s, err=%v", ifName, err)
m.lgr.Warnf("[lan] create socket error: if=%s, err=%v", ifName, err)
return
}
@@ -944,7 +944,7 @@ func (m *MIoTLan) send(ifName, targetIP string, data []byte) {
} else {
conn, ok := m.socks[ifName]
if !ok {
m.lgr.Infof("[lan] invalid socket: if=%s", ifName)
m.lgr.Debugf("[lan] invalid socket: if=%s", ifName)
return
}
conn.WriteToUDP(data, &net.UDPAddr{IP: net.ParseIP(targetIP), Port: otPort})
@@ -1118,7 +1118,7 @@ func (m *MIoTLan) handleRawMessage(data []byte, dataLen int, ip, ifName string)
if dataLen > otProbeLen {
msg, err := dev.DecryptPacket(data[:dataLen])
if err != nil {
m.lgr.Infof("[lan] decrypt error: did=%s, err=%v", did, err)
m.lgr.Debugf("[lan] decrypt error: did=%s, err=%v", did, err)
return
}
m.handleMessage(did, msg)
@@ -1128,7 +1128,7 @@ func (m *MIoTLan) handleRawMessage(data []byte, dataLen int, ip, ifName string)
func (m *MIoTLan) handleMessage(did string, msg map[string]interface{}) {
msgID, ok := msg["id"]
if !ok {
m.lgr.Infof("[lan] message without id: did=%s, msg=%v", did, msg)
m.lgr.Debugf("[lan] message without id: did=%s, msg=%v", did, msg)
return
}
id, ok := toInt(msgID)
@@ -1169,7 +1169,7 @@ func (m *MIoTLan) handleMessage(did string, msg map[string]interface{}) {
return
}
m.lgr.Infof("[lan] message: did=%s, method=%s", did, method)
m.lgr.Debugf("[lan] message: did=%s, method=%s", did, method)
switch method {
case "properties_changed":
@@ -1264,7 +1264,7 @@ func (m *MIoTLan) scanDevices() {
scanTime := m.nextScanTime()
m.scanTimer = time.AfterFunc(time.Duration(scanTime*float64(time.Second)), m.scanDevices)
m.lgr.Infof("[lan] next scan in %.0fs", scanTime)
m.lgr.Debugf("[lan] next scan in %.0fs", scanTime)
}
func (m *MIoTLan) nextScanTime() float64 {
@@ -1411,7 +1411,7 @@ func (m *MIoTLan) UpdateDevices(devices map[string]map[string]interface{}) {
}
model, _ := info["model"].(string)
if model != "" && m.profileModels[model] {
m.lgr.Infof("[lan] model not supported for LAN ctrl: did=%s, model=%s", did, model)
m.lgr.Debugf("[lan] model not supported for LAN ctrl: did=%s, model=%s", did, model)
continue
}
if existing, ok := m.devices[did]; ok {
@@ -1419,13 +1419,13 @@ func (m *MIoTLan) UpdateDevices(devices map[string]map[string]interface{}) {
} else {
tokenStr, _ := info["token"].(string)
if len(tokenStr) != 32 {
m.lgr.Infof("[lan] invalid token for device: did=%s", did)
m.lgr.Debugf("[lan] invalid token for device: did=%s", did)
continue
}
ip, _ := info["ip"].(string)
dev, err := newLanDevice(m, did, tokenStr, ip)
if err != nil {
m.lgr.Infof("[lan] create device error: did=%s, err=%v", did, err)
m.lgr.Warnf("[lan] create device error: did=%s, err=%v", did, err)
continue
}
m.devices[did] = dev
@@ -1491,7 +1491,7 @@ func (m *MIoTLan) SubProp(did string, handler func(map[string]interface{}, inter
}
m.cmdCh <- func() {
m.matcher.Sub(key, handler, ctx)
m.lgr.Infof("[lan] sub prop: key=%s", key)
m.lgr.Debugf("[lan] sub prop: key=%s", key)
}
}
@@ -1503,7 +1503,7 @@ func (m *MIoTLan) UnsubProp(did string, siid, piid int) {
}
m.cmdCh <- func() {
m.matcher.Unsub(key)
m.lgr.Infof("[lan] unsub prop: key=%s", key)
m.lgr.Debugf("[lan] unsub prop: key=%s", key)
}
}
@@ -1515,7 +1515,7 @@ func (m *MIoTLan) SubEvent(did string, handler func(map[string]interface{}, inte
}
m.cmdCh <- func() {
m.matcher.Sub(key, handler, ctx)
m.lgr.Infof("[lan] sub event: key=%s", key)
m.lgr.Debugf("[lan] sub event: key=%s", key)
}
}
@@ -1527,7 +1527,7 @@ func (m *MIoTLan) UnsubEvent(did string, siid, eiid int) {
}
m.cmdCh <- func() {
m.matcher.Unsub(key)
m.lgr.Infof("[lan] unsub event: key=%s", key)
m.lgr.Debugf("[lan] unsub event: key=%s", key)
}
}