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:
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user