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
+94 -33
View File
@@ -24,10 +24,14 @@ func newValMapper(vl miot.MIoTSpecValueList) *valMapper {
}
for _, item := range vl {
v := toInt(item.Value)
name := strings.ToLower(item.Desc)
vm.val2name[v] = name
vm.name2val[name] = v
vm.names = append(vm.names, name)
display := item.Desc
if display == "" {
display = item.Name
}
key := strings.ToLower(display)
vm.val2name[v] = display // 原始描述做展示
vm.name2val[key] = v // 小写 key 做匹配
vm.names = append(vm.names, display)
}
return vm
}
@@ -39,7 +43,7 @@ func (vm *valMapper) name(v int) string {
func (vm *valMapper) val(name string) (int, bool) {
if vm == nil { return 0, false }
v, ok := vm.name2val[name]
v, ok := vm.name2val[strings.ToLower(name)]
return v, ok
}
@@ -114,14 +118,19 @@ type AirConditioner interface {
SetEco(ctx context.Context, on bool) error
GetEco(ctx context.Context) (bool, error)
// 功率
HasPower() bool
GetElectricPower(ctx context.Context) (float64, error) // 当前电功率(W)
GetPowerConsumption(ctx context.Context) (float64, error) // 累计耗电量(kWh)
// 订阅
OnStateChanged(handler func(state ACState, changed string)) (string, error)
GetState() ACState
FetchState(ctx context.Context) (ACState, error)
OnPropsChanged(handler func(props ACProps, changed string)) (string, error)
GetProps() ACProps
FetchProps(ctx context.Context) (ACProps, error)
}
// ACState 空调完整状态(字段名对齐 SPEC type)
type ACState struct {
// ACProps 空调完整状态(字段名对齐 SPEC type)
type ACProps struct {
On bool `json:"on"`
Mode string `json:"mode"` // SPEC ValueList 解析后的名称:cool/heat/fan/dry/auto
TargetTemperature float64 `json:"targetTemperature"`
@@ -132,6 +141,8 @@ type ACState struct {
Swing string `json:"swing"` // SPEC ValueList 解析后的名称:off/vertical/horizontal/both
AuxHeat bool `json:"auxHeat"`
Eco bool `json:"eco"`
ElectricPower float64 `json:"electricPower"` // 当前电功率(W),来自 electric-power
PowerConsumption float64 `json:"powerConsumption"` // 累计耗电量(kWh),来自 power-consumption
}
type acDevice struct {
@@ -146,12 +157,14 @@ type acDevice struct {
propCurrentHumidity siidPiid // "relative-humidity"
propAuxHeat siidPiid // "aux-heat" or "heater"
propEco siidPiid // "eco"
propElectricPower siidPiid // "electric-power"
propPowerConsumption siidPiid // "power-consumption"
modeMap *valMapper // SPEC value→name for mode
fanMap *valMapper // SPEC value→name for fan-level
swingMap *valMapper // SPEC value→name for swing
stateCache ACState // MQTT 推送维护的本地缓存
propsCache ACProps // MQTT 推送维护的本地缓存
}
// NewAirConditioner creates an AirConditioner control for the given device.
@@ -233,6 +246,12 @@ func NewAirConditioner(client *xiaomi.Client, info *xiaomi.DeviceInfo) (AirCondi
if s, p, ok := resolver.find("eco"); ok {
d.propEco = siidPiid{s, p, true}
}
if s, p, ok := resolver.find("electric-power"); ok {
d.propElectricPower = siidPiid{s, p, true}
}
if s, p, ok := resolver.find("power-consumption"); ok {
d.propPowerConsumption = siidPiid{s, p, true}
}
return d, nil
}
@@ -270,6 +289,7 @@ func (d *acDevice) HasSwing() bool { return d.propSwing.valid }
func (d *acDevice) HasHumidity() bool { return d.propTargetHumidity.valid }
func (d *acDevice) HasAuxHeat() bool { return d.propAuxHeat.valid }
func (d *acDevice) HasEco() bool { return d.propEco.valid }
func (d *acDevice) HasPower() bool { return d.propElectricPower.valid || d.propPowerConsumption.valid }
func (d *acDevice) SetMode(ctx context.Context, mode string) error {
if !d.propMode.valid {
@@ -437,15 +457,37 @@ func (d *acDevice) GetEco(ctx context.Context) (bool, error) {
return false, nil
}
func (d *acDevice) GetElectricPower(ctx context.Context) (float64, error) {
if !d.propElectricPower.valid {
return 0, xiaomi.ErrNotSupported
}
prop, err := d.GetProp(ctx, d.propElectricPower.siid, d.propElectricPower.piid)
if err != nil {
return 0, err
}
return toFloat(prop.Value), nil
}
func (d *acDevice) GetPowerConsumption(ctx context.Context) (float64, error) {
if !d.propPowerConsumption.valid {
return 0, xiaomi.ErrNotSupported
}
prop, err := d.GetProp(ctx, d.propPowerConsumption.siid, d.propPowerConsumption.piid)
if err != nil {
return 0, err
}
return toFloat(prop.Value), nil
}
// propField maps a SPEC short name to a siid/piid pair and a cache-update function.
type propField struct {
siid, piid int
name string
fill func(*ACState, interface{})
fill func(*ACProps, interface{})
valType byte // 'b'=bool, 'i'=int, 'f'=float, 's'=string
}
func (d *acDevice) OnStateChanged(handler func(state ACState, changed string)) (string, error) {
func (d *acDevice) OnPropsChanged(handler func(props ACProps, changed string)) (string, error) {
fields := d.propFields()
if len(fields) == 0 {
return "", xiaomi.ErrNotSupported
@@ -467,8 +509,8 @@ func (d *acDevice) OnStateChanged(handler func(state ACState, changed string)) (
return
}
}
fields[idx].fill(&d.stateCache, prop.Value)
handler(d.stateCache, f.name)
fields[idx].fill(&d.propsCache, prop.Value)
handler(d.propsCache, f.name)
})
if err != nil {
return "", err
@@ -483,7 +525,7 @@ func (d *acDevice) OnStateChanged(handler func(state ACState, changed string)) (
func (d *acDevice) propFields() []propField {
r := d.getSpecResolver()
var fields []propField
add := func(sp siidPiid, name string, specType string, fill func(*ACState, interface{})) {
add := func(sp siidPiid, name string, specType string, fill func(*ACProps, interface{})) {
if sp.valid {
vt := byte(0)
switch specType {
@@ -499,22 +541,24 @@ func (d *acDevice) propFields() []propField {
fields = append(fields, propField{sp.siid, sp.piid, name, fill, vt})
}
}
add(d.propOn, "on", specFormat(r, "on"), func(s *ACState, v interface{}) { s.On, _ = v.(bool) })
add(d.propMode, "mode", specFormat(r, "mode"), func(s *ACState, v interface{}) {
add(d.propOn, "on", specFormat(r, "on"), func(s *ACProps, v interface{}) { s.On, _ = v.(bool) })
add(d.propMode, "mode", specFormat(r, "mode"), func(s *ACProps, v interface{}) {
s.Mode = d.modeMap.name(toInt(v))
})
add(d.propTargetTemp, "target-temperature", specFormat(r, "target-temperature"), func(s *ACState, v interface{}) { s.TargetTemperature = toFloat(v) })
add(d.propCurrentTemp, "temperature", specFormat(r, "temperature"), func(s *ACState, v interface{}) { s.Temperature = toFloat(v) })
add(d.propFanSpeed, "fan-level", specFormat(r, "fan-level"), func(s *ACState, v interface{}) {
add(d.propTargetTemp, "target-temperature", specFormat(r, "target-temperature"), func(s *ACProps, v interface{}) { s.TargetTemperature = toFloat(v) })
add(d.propCurrentTemp, "temperature", specFormat(r, "temperature"), func(s *ACProps, v interface{}) { s.Temperature = toFloat(v) })
add(d.propFanSpeed, "fan-level", specFormat(r, "fan-level"), func(s *ACProps, v interface{}) {
s.FanLevel = d.fanMap.name(toInt(v))
})
add(d.propSwing, "swing", specFormat(r, "swing-mode"), func(s *ACState, v interface{}) {
add(d.propSwing, "swing", specFormat(r, "swing-mode"), func(s *ACProps, v interface{}) {
s.Swing = d.swingMap.name(toInt(v))
})
add(d.propTargetHumidity, "target-humidity", specFormat(r, "target-humidity"), func(s *ACState, v interface{}) { s.TargetHumidity = int(toFloat(v)) })
add(d.propCurrentHumidity, "relative-humidity", specFormat(r, "relative-humidity"), func(s *ACState, v interface{}) { s.CurrentHumidity = int(toFloat(v)) })
add(d.propAuxHeat, "heater", specFormat(r, "heater"), func(s *ACState, v interface{}) { s.AuxHeat, _ = v.(bool) })
add(d.propEco, "eco", specFormat(r, "eco"), func(s *ACState, v interface{}) { s.Eco, _ = v.(bool) })
add(d.propTargetHumidity, "target-humidity", specFormat(r, "target-humidity"), func(s *ACProps, v interface{}) { s.TargetHumidity = int(toFloat(v)) })
add(d.propCurrentHumidity, "relative-humidity", specFormat(r, "relative-humidity"), func(s *ACProps, v interface{}) { s.CurrentHumidity = int(toFloat(v)) })
add(d.propAuxHeat, "heater", specFormat(r, "heater"), func(s *ACProps, v interface{}) { s.AuxHeat, _ = v.(bool) })
add(d.propEco, "eco", specFormat(r, "eco"), func(s *ACProps, v interface{}) { s.Eco, _ = v.(bool) })
add(d.propElectricPower, "electric-power", specFormat(r, "electric-power"), func(s *ACProps, v interface{}) { s.ElectricPower = toFloat(v) })
add(d.propPowerConsumption, "power-consumption", specFormat(r, "power-consumption"), func(s *ACProps, v interface{}) { s.PowerConsumption = toFloat(v) })
return fields
}
@@ -540,9 +584,26 @@ func toFloat(v interface{}) float64 {
return 0
}
func (d *acDevice) GetState() ACState { return d.stateCache }
// toBool converts an interface{} to bool. 支持 bool 及数值(0/1)类型。
func toBool(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
}
return false
}
func (d *acDevice) FetchState(ctx context.Context) (ACState, error) {
func (d *acDevice) GetProps() ACProps { return d.propsCache }
func (d *acDevice) FetchProps(ctx context.Context) (ACProps, error) {
fields := d.propFields()
keys := make([]xiaomi.PropKey, len(fields))
for i, f := range fields {
@@ -550,17 +611,17 @@ func (d *acDevice) FetchState(ctx context.Context) (ACState, error) {
}
props, err := d.client.GetProps(ctx, keys)
if err != nil {
return d.stateCache, err
return d.propsCache, err
}
state := ACState{}
result := ACProps{}
for _, p := range props {
for _, f := range fields {
if f.siid == p.SIID && f.piid == p.PIID {
f.fill(&state, p.Value)
f.fill(&result, p.Value)
break
}
}
}
d.stateCache = state
return state, nil
d.propsCache = result
return result, nil
}