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
}
+26
View File
@@ -55,19 +55,31 @@ func (d *BaseDevice) DID() string {
// Name returns the device's display name.
func (d *BaseDevice) Name() string {
d.refreshInfo()
return d.info.Name
}
// Model returns the device's model identifier.
func (d *BaseDevice) Model() string {
d.refreshInfo()
return d.info.Model
}
// Online returns whether the device is currently online.
func (d *BaseDevice) Online() bool {
d.refreshInfo()
return d.info.Online
}
// refreshInfo 从客户端缓存拉取最新设备信息,有变化时原地更新。
func (d *BaseDevice) refreshInfo() {
info, err := d.client.GetDevice(context.Background(), d.info.DID)
if err != nil || info == nil {
return
}
d.info = info
}
// Info returns the full device information.
func (d *BaseDevice) Info() *xiaomi.DeviceInfo {
return d.info
@@ -83,6 +95,12 @@ func (d *BaseDevice) SetProp(ctx context.Context, siid, piid int, value interfac
return d.client.SetProp(ctx, d.info.DID, siid, piid, value)
}
// Ping 通过实际读属性验证设备是否在线,不走缓存。
func (d *BaseDevice) Ping(ctx context.Context, siid, piid int) bool {
_, err := d.GetProp(ctx, siid, piid)
return err == nil
}
// Action invokes a device action.
func (d *BaseDevice) Action(ctx context.Context, siid, aiid int, params []interface{}) (*xiaomi.ActionResult, error) {
return d.client.Action(ctx, d.info.DID, siid, aiid, params)
@@ -168,6 +186,14 @@ func (r *specResolver) find(propType string) (siid, piid int, found bool) {
return 0, 0, false
}
// prop returns the full SPEC property for the given type name.
func (r *specResolver) prop(propType string) *miot.MIoTSpecProperty {
if r == nil {
return nil
}
return r.byType[propType]
}
// format returns the SPEC Format for a property type ("bool", "uint8", "float", "string").
func (r *specResolver) format(propType string) string {
if r == nil {
+11 -11
View File
@@ -15,9 +15,9 @@ type Cover interface {
SetPosition(ctx context.Context, pct int) error
GetPosition(ctx context.Context) (int, error)
OnStateChanged(handler func(position int)) (string, error)
GetState() int
FetchState(ctx context.Context) (int, error)
OnPropsChanged(handler func(position int)) (string, error)
GetProps() int
FetchProps(ctx context.Context) (int, error)
}
type coverDevice struct {
@@ -25,7 +25,7 @@ type coverDevice struct {
propMotor siidPiid
propPosition siidPiid
stateCache int
propsCache int
}
const (
@@ -74,20 +74,20 @@ func (d *coverDevice) GetPosition(ctx context.Context) (int, error) {
return toInt(prop.Value), nil
}
func (d *coverDevice) OnStateChanged(handler func(position int)) (string, error) {
func (d *coverDevice) OnPropsChanged(handler func(position int)) (string, error) {
if !d.propPosition.valid { return "", xiaomi.ErrNotSupported }
return d.SubProp(0, 0, func(did string, prop *xiaomi.PropertyValue) {
if prop.SIID == d.propPosition.siid && prop.PIID == d.propPosition.piid {
d.stateCache = toInt(prop.Value)
handler(d.stateCache)
d.propsCache = toInt(prop.Value)
handler(d.propsCache)
}
})
}
func (d *coverDevice) GetState() int { return d.stateCache }
func (d *coverDevice) FetchState(ctx context.Context) (int, error) {
func (d *coverDevice) GetProps() int { return d.propsCache }
func (d *coverDevice) FetchProps(ctx context.Context) (int, error) {
pos, err := d.GetPosition(ctx)
if err != nil { return d.stateCache, err }
d.stateCache = pos
if err != nil { return d.propsCache, err }
d.propsCache = pos
return pos, nil
}
+7 -3
View File
@@ -69,8 +69,10 @@ func createImpl(client *xiaomi.Client, info *xiaomi.DeviceInfo) (interface{}, er
ctrl, err = NewAirConditioner(client, info)
case matchURN(urn, "light", "lightbulb", "night-light", "ceiling-light"):
ctrl, err = NewLight(client, info)
case matchURN(urn, "switch", "outlet", "powerstrip", "toggle-switch", "wireless-switch", "curtain-switch"):
case matchURN(urn, "switch", "powerstrip", "toggle-switch", "wireless-switch", "curtain-switch"):
ctrl, err = NewSwitch(client, info)
case matchURN(urn, "plug", "outlet"):
ctrl, err = NewPlug(client, info)
case matchURN(urn, "fan", "ceiling-fan", "ventilator", "air-fresh", "air-purifier"):
ctrl, err = NewFan(client, info)
case matchURN(urn, "curtain", "airer-rack", "airer"):
@@ -83,12 +85,14 @@ func createImpl(client *xiaomi.Client, info *xiaomi.DeviceInfo) (interface{}, er
ctrl, err = NewWaterHeater(client, info)
case matchURN(urn, "heater", "electric-blanket", "thermostat"):
ctrl, err = NewThermostat(client, info)
case matchURN(urn, "sensor-occupy", "motion-sensor", "magnet"):
case matchURN(urn, "sensor-occupy", "sensor_occupy", "occupancy-sensor", "occupancy_sensor", "motion-sensor", "motion_sensor", "magnet"):
ctrl, err = NewOccupancySensor(client, info)
case matchURN(urn, "sensor-temp", "sensor-humidity", "sensor-ht", "sensor-smoke", "sensor-gasm", "illumination"):
case matchURN(urn, "sensor-temp", "sensor_temp", "sensor-humidity", "sensor_humidity", "sensor-ht", "sensor_ht", "sensor-smoke", "sensor-gasm", "illumination"):
ctrl, err = NewTempHumiditySensor(client, info)
case matchURN(urn, "speaker", "bluetooth-speaker", "alarm-clock", "media-player", "playback", "music"):
ctrl, err = NewSpeaker(client, info)
case matchURN(urn, "gateway"):
ctrl, err = NewGateway(client, info)
default:
// Not a supported device type — not an error, just returns nil
return nil, nil
+152 -37
View File
@@ -2,34 +2,47 @@ package devices
import (
"context"
"xiaomihome/miot"
"xiaomihome/xiaomi"
)
// Fan 风扇统一控制接口
// Fan 风扇/新风机统一控制接口。
// 风扇和新风都有 mode 和 fan-level,但部分新风机没有 fan-level,此时用 mode 控制。
type Fan interface {
Device
TurnOn(ctx context.Context) error
TurnOff(ctx context.Context) error
IsOn(ctx context.Context) (bool, error)
SetFanLevel(ctx context.Context, level int) error // 0=off, 1-4 档位
// 风速控制(有 fan-level 时可用)
HasFanLevel() bool
FanLevelValues() []int
SetFanLevel(ctx context.Context, level int) error
GetFanLevel(ctx context.Context) (int, error)
// 模式控制(有 mode+ValueList 时可用,返回原始 uint8 值)
HasMode() bool
ModeValues() []int
SetMode(ctx context.Context, mode int) error
GetMode(ctx context.Context) (int, error)
SetOscillation(ctx context.Context, on bool) error
IsOscillating(ctx context.Context) (bool, error)
SetNaturalWind(ctx context.Context, on bool) error
IsNaturalWind(ctx context.Context) (bool, error)
OnStateChanged(handler func(state FanState, changed string)) (string, error)
GetState() FanState
FetchState(ctx context.Context) (FanState, error)
OnPropsChanged(handler func(props FanProps, changed string)) (string, error)
GetProps() FanProps
FetchProps(ctx context.Context) (FanProps, error)
}
// FanState 风扇完整状态(MQTT 缓存)
type FanState struct {
// FanProps 风扇/新风机完整状态(MQTT 缓存)
type FanProps struct {
On bool `json:"on"`
FanLevel int `json:"fanLevel"`
Mode int `json:"mode"`
Oscillation bool `json:"oscillation"`
NaturalWind bool `json:"naturalWind"`
}
@@ -38,10 +51,14 @@ type fanDevice struct {
BaseDevice
propOn siidPiid // "on"
propFanLevel siidPiid // "fan-level"
propMode siidPiid // "mode" — 有 ValueList 时为模式选择器
propNaturalWind siidPiid // "mode" — 无 ValueList 时为自然风开关
propOscillation siidPiid // "horizontal-swing" or "oscillation"
propNaturalWind siidPiid // "mode" (natural wind mode)
stateCache FanState
modeValues []int // 可用模式值列表(来自 SPEC value-list)
fanLevelValues []int // 可用风速档位值列表
propsCache FanProps
}
// NewFan creates a Fan control for the given device.
@@ -63,12 +80,36 @@ func NewFan(client *xiaomi.Client, info *xiaomi.DeviceInfo) (Fan, error) {
if s, p, ok := resolver.find("fan-level"); ok {
d.propFanLevel = siidPiid{s, p, true}
if prop := resolver.prop("fan-level"); prop != nil {
if len(prop.ValueList) > 0 {
for _, item := range prop.ValueList {
d.fanLevelValues = append(d.fanLevelValues, toInt(item.Value))
}
} else if prop.ValueRange != nil {
d.fanLevelValues = rangeValues(prop.ValueRange)
}
}
}
if s, p, ok := resolver.find("horizontal-swing"); ok {
d.propOscillation = siidPiid{s, p, true}
}
// mode 属性:有 ValueList → 模式选择器,有 ValueRange → 连续值,否则 → 自然风开关
if s, p, ok := resolver.find("mode"); ok {
d.propNaturalWind = siidPiid{s, p, true}
if prop := resolver.prop("mode"); prop != nil {
if len(prop.ValueList) > 0 {
d.propMode = siidPiid{s, p, true}
for _, item := range prop.ValueList {
d.modeValues = append(d.modeValues, toInt(item.Value))
}
} else if prop.ValueRange != nil {
d.propMode = siidPiid{s, p, true}
d.modeValues = rangeValues(prop.ValueRange)
}
}
if !d.propMode.valid {
d.propNaturalWind = siidPiid{s, p, true}
}
}
return d, nil
@@ -102,6 +143,10 @@ func (d *fanDevice) IsOn(ctx context.Context) (bool, error) {
return false, nil
}
func (d *fanDevice) HasFanLevel() bool { return d.propFanLevel.valid }
func (d *fanDevice) FanLevelValues() []int { return d.fanLevelValues }
func (d *fanDevice) SetFanLevel(ctx context.Context, level int) error {
if !d.propFanLevel.valid {
return xiaomi.ErrNotSupported
@@ -120,6 +165,28 @@ func (d *fanDevice) GetFanLevel(ctx context.Context) (int, error) {
return toInt(prop.Value), nil
}
func (d *fanDevice) HasMode() bool { return d.propMode.valid }
func (d *fanDevice) ModeValues() []int { return d.modeValues }
func (d *fanDevice) SetMode(ctx context.Context, mode int) error {
if !d.propMode.valid {
return xiaomi.ErrNotSupported
}
return d.SetProp(ctx, d.propMode.siid, d.propMode.piid, mode)
}
func (d *fanDevice) GetMode(ctx context.Context) (int, error) {
if !d.propMode.valid {
return 0, xiaomi.ErrNotSupported
}
prop, err := d.GetProp(ctx, d.propMode.siid, d.propMode.piid)
if err != nil {
return 0, err
}
return toInt(prop.Value), nil
}
func (d *fanDevice) SetOscillation(ctx context.Context, on bool) error {
if !d.propOscillation.valid {
return xiaomi.ErrNotSupported
@@ -145,11 +212,11 @@ func (d *fanDevice) SetNaturalWind(ctx context.Context, on bool) error {
if !d.propNaturalWind.valid {
return xiaomi.ErrNotSupported
}
mode := 0 // normal
v := 0
if on {
mode = 1 // natural wind
v = 1
}
return d.SetProp(ctx, d.propNaturalWind.siid, d.propNaturalWind.piid, mode)
return d.SetProp(ctx, d.propNaturalWind.siid, d.propNaturalWind.piid, v)
}
func (d *fanDevice) IsNaturalWind(ctx context.Context) (bool, error) {
@@ -163,57 +230,105 @@ func (d *fanDevice) IsNaturalWind(ctx context.Context) (bool, error) {
return toInt(prop.Value) == 1, nil
}
func (d *fanDevice) OnStateChanged(handler func(state FanState, changed string)) (string, error) {
type field struct{ siid, piid int; name string; fill func(*FanState, interface{}) }
func (d *fanDevice) OnPropsChanged(handler func(props FanProps, changed string)) (string, error) {
type field struct {
siid, piid int
name string
fill func(*FanProps, interface{})
}
var fields []field
if d.propOn.valid {
fields = append(fields, field{d.propOn.siid, d.propOn.piid, "on",
func(s *FanState, v interface{}) { s.On, _ = v.(bool) }})
func(s *FanProps, v interface{}) { s.On, _ = v.(bool) }})
}
if d.propFanLevel.valid {
fields = append(fields, field{d.propFanLevel.siid, d.propFanLevel.piid, "fan-level",
func(s *FanState, v interface{}) { s.FanLevel = toInt(v) }})
func(s *FanProps, v interface{}) { s.FanLevel = toInt(v) }})
}
if d.propMode.valid {
fields = append(fields, field{d.propMode.siid, d.propMode.piid, "mode",
func(s *FanProps, v interface{}) { s.Mode = toInt(v) }})
}
if d.propOscillation.valid {
fields = append(fields, field{d.propOscillation.siid, d.propOscillation.piid, "swing",
func(s *FanState, v interface{}) { s.Oscillation, _ = v.(bool) }})
fields = append(fields, field{d.propOscillation.siid, d.propOscillation.piid, "oscillation",
func(s *FanProps, v interface{}) { s.Oscillation, _ = v.(bool) }})
}
if d.propNaturalWind.valid {
fields = append(fields, field{d.propNaturalWind.siid, d.propNaturalWind.piid, "mode",
func(s *FanState, v interface{}) { s.NaturalWind = toInt(v) == 1 }})
fields = append(fields, field{d.propNaturalWind.siid, d.propNaturalWind.piid, "natural-wind",
func(s *FanProps, v interface{}) { s.NaturalWind = toInt(v) == 1 }})
}
if len(fields) == 0 {
return "", xiaomi.ErrNotSupported
}
return d.SubProp(0, 0, func(did string, prop *xiaomi.PropertyValue) {
for _, f := range fields {
if f.siid == prop.SIID && f.piid == prop.PIID {
f.fill(&d.stateCache, prop.Value)
handler(d.stateCache, f.name)
f.fill(&d.propsCache, prop.Value)
handler(d.propsCache, f.name)
return
}
}
})
}
func (d *fanDevice) GetState() FanState { return d.stateCache }
func (d *fanDevice) FetchState(ctx context.Context) (FanState, error) {
func (d *fanDevice) GetProps() FanProps { return d.propsCache }
func (d *fanDevice) FetchProps(ctx context.Context) (FanProps, error) {
keys := []xiaomi.PropKey{}
if d.propOn.valid { keys = append(keys, xiaomi.PropKey{DID: d.info.DID, SIID: d.propOn.siid, PIID: d.propOn.piid}) }
if d.propFanLevel.valid { keys = append(keys, xiaomi.PropKey{DID: d.info.DID, SIID: d.propFanLevel.siid, PIID: d.propFanLevel.piid}) }
if d.propOscillation.valid { keys = append(keys, xiaomi.PropKey{DID: d.info.DID, SIID: d.propOscillation.siid, PIID: d.propOscillation.piid}) }
if d.propNaturalWind.valid { keys = append(keys, xiaomi.PropKey{DID: d.info.DID, SIID: d.propNaturalWind.siid, PIID: d.propNaturalWind.piid}) }
if d.propOn.valid {
keys = append(keys, xiaomi.PropKey{DID: d.info.DID, SIID: d.propOn.siid, PIID: d.propOn.piid})
}
if d.propFanLevel.valid {
keys = append(keys, xiaomi.PropKey{DID: d.info.DID, SIID: d.propFanLevel.siid, PIID: d.propFanLevel.piid})
}
if d.propMode.valid {
keys = append(keys, xiaomi.PropKey{DID: d.info.DID, SIID: d.propMode.siid, PIID: d.propMode.piid})
}
if d.propOscillation.valid {
keys = append(keys, xiaomi.PropKey{DID: d.info.DID, SIID: d.propOscillation.siid, PIID: d.propOscillation.piid})
}
if d.propNaturalWind.valid {
keys = append(keys, xiaomi.PropKey{DID: d.info.DID, SIID: d.propNaturalWind.siid, PIID: d.propNaturalWind.piid})
}
props, err := d.client.GetProps(ctx, keys)
if err != nil { return d.stateCache, err }
state := FanState{}
if err != nil {
return d.propsCache, err
}
result := FanProps{}
for _, p := range props {
switch {
case d.propOn.valid && p.SIID == d.propOn.siid && p.PIID == d.propOn.piid: state.On, _ = p.Value.(bool)
case d.propFanLevel.valid && p.SIID == d.propFanLevel.siid && p.PIID == d.propFanLevel.piid: state.FanLevel = toInt(p.Value)
case d.propOscillation.valid && p.SIID == d.propOscillation.siid && p.PIID == d.propOscillation.piid: state.Oscillation, _ = p.Value.(bool)
case d.propNaturalWind.valid && p.SIID == d.propNaturalWind.siid && p.PIID == d.propNaturalWind.piid: state.NaturalWind = toInt(p.Value) == 1
case d.propOn.valid && p.SIID == d.propOn.siid && p.PIID == d.propOn.piid:
result.On, _ = p.Value.(bool)
case d.propFanLevel.valid && p.SIID == d.propFanLevel.siid && p.PIID == d.propFanLevel.piid:
result.FanLevel = toInt(p.Value)
case d.propMode.valid && p.SIID == d.propMode.siid && p.PIID == d.propMode.piid:
result.Mode = toInt(p.Value)
case d.propOscillation.valid && p.SIID == d.propOscillation.siid && p.PIID == d.propOscillation.piid:
result.Oscillation, _ = p.Value.(bool)
case d.propNaturalWind.valid && p.SIID == d.propNaturalWind.siid && p.PIID == d.propNaturalWind.piid:
result.NaturalWind = toInt(p.Value) == 1
}
}
d.stateCache = state
return state, nil
d.propsCache = result
return result, nil
}
// rangeValues 从 SPEC value-range 生成离散值列表,如 [1,8,1] → [1,2,3,4,5,6,7,8]
func rangeValues(r *miot.MIoTSpecValueRange) []int {
if r == nil {
return nil
}
min, max, step := toInt(r.Min), toInt(r.Max), toInt(r.Step)
if step <= 0 {
step = 1
}
if max > 100 {
return nil // 太大会爆内存,返回 nil 让调用方自行处理
}
var vals []int
for i := min; i <= max; i += step {
vals = append(vals, i)
}
return vals
}
+22 -22
View File
@@ -16,13 +16,13 @@ type Humidifier interface {
SetFanLevel(ctx context.Context, level int) error
GetFanLevel(ctx context.Context) (int, error)
OnStateChanged(handler func(state HumidifierState, changed string)) (string, error)
GetState() HumidifierState
FetchState(ctx context.Context) (HumidifierState, error)
OnPropsChanged(handler func(props HumidifierProps, changed string)) (string, error)
GetProps() HumidifierProps
FetchProps(ctx context.Context) (HumidifierProps, error)
}
// HumidifierState 加湿器状态(MQTT 缓存)
type HumidifierState struct {
// HumidifierProps 加湿器状态(MQTT 缓存)
type HumidifierProps struct {
On bool `json:"on"`
TargetHumidity int `json:"targetHumidity"`
FanLevel int `json:"fanLevel"`
@@ -33,7 +33,7 @@ type humidifierDevice struct {
propOn siidPiid
propTargetHumidity siidPiid
propFanLevel siidPiid
stateCache HumidifierState
propsCache HumidifierProps
}
func NewHumidifier(client *xiaomi.Client, info *xiaomi.DeviceInfo) (Humidifier, error) {
@@ -84,46 +84,46 @@ func (d *humidifierDevice) GetFanLevel(ctx context.Context) (int, error) {
return toInt(prop.Value), nil
}
func (d *humidifierDevice) OnStateChanged(handler func(state HumidifierState, changed string)) (string, error) {
type field struct{ siid, piid int; name string; fill func(*HumidifierState, interface{}) }
func (d *humidifierDevice) OnPropsChanged(handler func(props HumidifierProps, changed string)) (string, error) {
type field struct{ siid, piid int; name string; fill func(*HumidifierProps, interface{}) }
var fields []field
if d.propOn.valid {
fields = append(fields, field{d.propOn.siid, d.propOn.piid, "on", func(s *HumidifierState, v interface{}) { s.On, _ = v.(bool) }})
fields = append(fields, field{d.propOn.siid, d.propOn.piid, "on", func(s *HumidifierProps, v interface{}) { s.On, _ = v.(bool) }})
}
if d.propTargetHumidity.valid {
fields = append(fields, field{d.propTargetHumidity.siid, d.propTargetHumidity.piid, "target-humidity", func(s *HumidifierState, v interface{}) { s.TargetHumidity = toInt(v) }})
fields = append(fields, field{d.propTargetHumidity.siid, d.propTargetHumidity.piid, "target-humidity", func(s *HumidifierProps, v interface{}) { s.TargetHumidity = toInt(v) }})
}
if d.propFanLevel.valid {
fields = append(fields, field{d.propFanLevel.siid, d.propFanLevel.piid, "fan-level", func(s *HumidifierState, v interface{}) { s.FanLevel = toInt(v) }})
fields = append(fields, field{d.propFanLevel.siid, d.propFanLevel.piid, "fan-level", func(s *HumidifierProps, v interface{}) { s.FanLevel = toInt(v) }})
}
if len(fields) == 0 { return "", xiaomi.ErrNotSupported }
return d.SubProp(0, 0, func(did string, prop *xiaomi.PropertyValue) {
for _, f := range fields {
if f.siid == prop.SIID && f.piid == prop.PIID {
f.fill(&d.stateCache, prop.Value)
handler(d.stateCache, f.name)
f.fill(&d.propsCache, prop.Value)
handler(d.propsCache, f.name)
return
}
}
})
}
func (d *humidifierDevice) GetState() HumidifierState { return d.stateCache }
func (d *humidifierDevice) FetchState(ctx context.Context) (HumidifierState, error) {
func (d *humidifierDevice) GetProps() HumidifierProps { return d.propsCache }
func (d *humidifierDevice) FetchProps(ctx context.Context) (HumidifierProps, error) {
keys := []xiaomi.PropKey{}
if d.propOn.valid { keys = append(keys, xiaomi.PropKey{DID: d.info.DID, SIID: d.propOn.siid, PIID: d.propOn.piid}) }
if d.propTargetHumidity.valid { keys = append(keys, xiaomi.PropKey{DID: d.info.DID, SIID: d.propTargetHumidity.siid, PIID: d.propTargetHumidity.piid}) }
if d.propFanLevel.valid { keys = append(keys, xiaomi.PropKey{DID: d.info.DID, SIID: d.propFanLevel.siid, PIID: d.propFanLevel.piid}) }
props, err := d.client.GetProps(ctx, keys)
if err != nil { return d.stateCache, err }
state := HumidifierState{}
if err != nil { return d.propsCache, err }
result := HumidifierProps{}
for _, p := range props {
switch {
case d.propOn.valid && p.SIID == d.propOn.siid && p.PIID == d.propOn.piid: state.On, _ = p.Value.(bool)
case d.propTargetHumidity.valid && p.SIID == d.propTargetHumidity.siid && p.PIID == d.propTargetHumidity.piid: state.TargetHumidity = toInt(p.Value)
case d.propFanLevel.valid && p.SIID == d.propFanLevel.siid && p.PIID == d.propFanLevel.piid: state.FanLevel = toInt(p.Value)
case d.propOn.valid && p.SIID == d.propOn.siid && p.PIID == d.propOn.piid: result.On, _ = p.Value.(bool)
case d.propTargetHumidity.valid && p.SIID == d.propTargetHumidity.siid && p.PIID == d.propTargetHumidity.piid: result.TargetHumidity = toInt(p.Value)
case d.propFanLevel.valid && p.SIID == d.propFanLevel.siid && p.PIID == d.propFanLevel.piid: result.FanLevel = toInt(p.Value)
}
}
d.stateCache = state
return state, nil
d.propsCache = result
return result, nil
}
+29 -24
View File
@@ -2,6 +2,8 @@ package devices
import (
"context"
"strconv"
"xiaomihome/xiaomi"
)
@@ -29,13 +31,13 @@ type Light interface {
SetColor(ctx context.Context, r, g, b int) error
GetColor(ctx context.Context) (r, g, b int, err error)
OnStateChanged(handler func(state LightState, changed string)) (string, error)
GetState() LightState
FetchState(ctx context.Context) (LightState, error)
OnPropsChanged(handler func(props LightProps, changed string)) (string, error)
GetProps() LightProps
FetchProps(ctx context.Context) (LightProps, error)
}
// LightState 灯光完整状态(字段名对齐 SPEC type)
type LightState struct {
// LightProps 灯光完整状态(字段名对齐 SPEC type)
type LightProps struct {
On bool `json:"on"` // SPEC: "on"
Brightness int `json:"brightness"` // SPEC: "brightness"
ColorTemperature int `json:"colorTemperature"` // SPEC: "color-temperature"
@@ -49,7 +51,7 @@ type lightDevice struct {
propColorTemp siidPiid // "color-temperature"
propColor siidPiid // "color"
stateCache LightState // MQTT 推送维护的本地缓存
propsCache LightProps // MQTT 推送维护的本地缓存
}
// NewLight creates a Light control for the given device.
@@ -183,28 +185,28 @@ func (d *lightDevice) GetColor(ctx context.Context) (r, g, b int, err error) {
return r, g, b, nil
}
func (d *lightDevice) OnStateChanged(handler func(state LightState, changed string)) (string, error) {
func (d *lightDevice) OnPropsChanged(handler func(props LightProps, changed string)) (string, error) {
type field struct {
siid, piid int
name string
fill func(*LightState, interface{})
fill func(*LightProps, interface{})
}
var fields []field
if d.propOn.valid {
fields = append(fields, field{d.propOn.siid, d.propOn.piid, "on",
func(s *LightState, v interface{}) { s.On, _ = v.(bool) }})
func(s *LightProps, v interface{}) { s.On, _ = v.(bool) }})
}
if d.propBrightness.valid {
fields = append(fields, field{d.propBrightness.siid, d.propBrightness.piid, "brightness",
func(s *LightState, v interface{}) { s.Brightness = toInt(v) }})
func(s *LightProps, v interface{}) { s.Brightness = toInt(v) }})
}
if d.propColorTemp.valid {
fields = append(fields, field{d.propColorTemp.siid, d.propColorTemp.piid, "color-temperature",
func(s *LightState, v interface{}) { s.ColorTemperature = toInt(v) }})
func(s *LightProps, v interface{}) { s.ColorTemperature = toInt(v) }})
}
if d.propColor.valid {
fields = append(fields, field{d.propColor.siid, d.propColor.piid, "color",
func(s *LightState, v interface{}) {
func(s *LightProps, v interface{}) {
if v, ok := v.(float64); ok {
c := int(v)
s.ColorRGB = [3]int{(c >> 16) & 0xFF, (c >> 8) & 0xFF, c & 0xFF}
@@ -217,8 +219,8 @@ func (d *lightDevice) OnStateChanged(handler func(state LightState, changed stri
return d.SubProp(0, 0, func(did string, prop *xiaomi.PropertyValue) {
for _, f := range fields {
if f.siid == prop.SIID && f.piid == prop.PIID {
f.fill(&d.stateCache, prop.Value)
handler(d.stateCache, f.name)
f.fill(&d.propsCache, prop.Value)
handler(d.propsCache, f.name)
return
}
}
@@ -240,29 +242,32 @@ func toInt(v interface{}) int {
return int(n)
case uint32:
return int(n)
case string:
i, _ := strconv.Atoi(n)
return i
}
return 0
}
func (d *lightDevice) GetState() LightState { return d.stateCache }
func (d *lightDevice) FetchState(ctx context.Context) (LightState, error) {
func (d *lightDevice) GetProps() LightProps { return d.propsCache }
func (d *lightDevice) FetchProps(ctx context.Context) (LightProps, error) {
keys := []xiaomi.PropKey{}
if d.propOn.valid { keys = append(keys, xiaomi.PropKey{DID: d.info.DID, SIID: d.propOn.siid, PIID: d.propOn.piid}) }
if d.propBrightness.valid { keys = append(keys, xiaomi.PropKey{DID: d.info.DID, SIID: d.propBrightness.siid, PIID: d.propBrightness.piid}) }
if d.propColorTemp.valid { keys = append(keys, xiaomi.PropKey{DID: d.info.DID, SIID: d.propColorTemp.siid, PIID: d.propColorTemp.piid}) }
if d.propColor.valid { keys = append(keys, xiaomi.PropKey{DID: d.info.DID, SIID: d.propColor.siid, PIID: d.propColor.piid}) }
props, err := d.client.GetProps(ctx, keys)
if err != nil { return d.stateCache, err }
state := LightState{}
if err != nil { return d.propsCache, err }
result := LightProps{}
for _, p := range props {
switch {
case d.propOn.valid && p.SIID == d.propOn.siid && p.PIID == d.propOn.piid: state.On, _ = p.Value.(bool)
case d.propBrightness.valid && p.SIID == d.propBrightness.siid && p.PIID == d.propBrightness.piid: state.Brightness = toInt(p.Value)
case d.propColorTemp.valid && p.SIID == d.propColorTemp.siid && p.PIID == d.propColorTemp.piid: state.ColorTemperature = toInt(p.Value)
case d.propOn.valid && p.SIID == d.propOn.siid && p.PIID == d.propOn.piid: result.On, _ = p.Value.(bool)
case d.propBrightness.valid && p.SIID == d.propBrightness.siid && p.PIID == d.propBrightness.piid: result.Brightness = toInt(p.Value)
case d.propColorTemp.valid && p.SIID == d.propColorTemp.siid && p.PIID == d.propColorTemp.piid: result.ColorTemperature = toInt(p.Value)
case d.propColor.valid && p.SIID == d.propColor.siid && p.PIID == d.propColor.piid:
if v, ok := p.Value.(float64); ok { c := int(v); state.ColorRGB = [3]int{(c>>16)&0xFF, (c>>8)&0xFF, c&0xFF} }
if v, ok := p.Value.(float64); ok { c := int(v); result.ColorRGB = [3]int{(c>>16)&0xFF, (c>>8)&0xFF, c&0xFF} }
}
}
d.stateCache = state
return state, nil
d.propsCache = result
return result, nil
}
+54 -30
View File
@@ -13,16 +13,25 @@ type OccupancySensor interface {
IsOccupied(ctx context.Context) (bool, error)
GetIlluminance(ctx context.Context) (float64, error)
OnStateChanged(handler func(occupied bool, illuminance float64)) (string, error)
GetState() (bool, float64)
FetchState(ctx context.Context) (bool, float64, error)
OnPropsChanged(handler func(occupied bool, illuminance float64)) (string, error)
GetProps() (bool, float64)
FetchProps(ctx context.Context) (bool, float64, error)
}
type occupancyDevice struct {
BaseDevice
propOccupancy siidPiid
propIlluminance siidPiid
stateCache [2]interface{} // [0]=bool occupied, [1]=float64 lux
propOccupancy siidPiid // "occupancy" / "occupancy-status"(bool/uint8)
propNoMotion siidPiid // "no-motion-duration"(uint16,linp.motion.* 等无 occupancy 属性的设备)
propIlluminance siidPiid // "illumination"
propsCache [2]interface{} // [0]=bool occupied, [1]=float64 lux
}
// occupied 根据设备支持的属性类型计算有人状态。
func (d *occupancyDevice) occupied(v interface{}) bool {
if d.propOccupancy.valid {
return toBool(v)
}
return toFloat(v) == 0 // no-motion-duration==0 → 有人
}
func NewOccupancySensor(client *xiaomi.Client, info *xiaomi.DeviceInfo) (OccupancySensor, error) {
@@ -35,22 +44,34 @@ func NewOccupancySensor(client *xiaomi.Client, info *xiaomi.DeviceInfo) (Occupan
for _, svc := range spec.Services {
for _, prop := range svc.Properties {
switch propertyShortName(prop.Type) {
case "occupancy":
case "occupancy", "occupancy-status":
if !d.propOccupancy.valid { d.propOccupancy = siidPiid{svc.IID, prop.PIID, true} }
case "no-motion-duration":
if !d.propNoMotion.valid { d.propNoMotion = siidPiid{svc.IID, prop.PIID, true} }
case "illumination":
if !d.propIlluminance.valid { d.propIlluminance = siidPiid{svc.IID, prop.PIID, true} }
}
}
}
if !d.propOccupancy.valid { return nil, fmt.Errorf("%w: no occupancy property in SPEC", xiaomi.ErrNotSupported) }
if !d.propOccupancy.valid && !d.propNoMotion.valid {
return nil, fmt.Errorf("%w: no occupancy/no-motion-duration property in SPEC", xiaomi.ErrNotSupported)
}
return d, nil
}
// occupiedProp 返回判断有人状态所用的属性。
func (d *occupancyDevice) occupiedProp() siidPiid {
if d.propOccupancy.valid {
return d.propOccupancy
}
return d.propNoMotion
}
func (d *occupancyDevice) IsOccupied(ctx context.Context) (bool, error) {
prop, err := d.GetProp(ctx, d.propOccupancy.siid, d.propOccupancy.piid)
p := d.occupiedProp()
prop, err := d.GetProp(ctx, p.siid, p.piid)
if err != nil { return false, err }
if b, ok := prop.Value.(bool); ok { return b, nil }
return false, nil
return d.occupied(prop.Value), nil
}
func (d *occupancyDevice) GetIlluminance(ctx context.Context) (float64, error) {
if !d.propIlluminance.valid { return 0, xiaomi.ErrNotSupported }
@@ -59,46 +80,49 @@ func (d *occupancyDevice) GetIlluminance(ctx context.Context) (float64, error) {
return toFloat(prop.Value), nil
}
func (d *occupancyDevice) OnStateChanged(handler func(occupied bool, illuminance float64)) (string, error) {
if !d.propOccupancy.valid && !d.propIlluminance.valid { return "", xiaomi.ErrNotSupported }
func (d *occupancyDevice) OnPropsChanged(handler func(occupied bool, illuminance float64)) (string, error) {
if !d.propOccupancy.valid && !d.propNoMotion.valid && !d.propIlluminance.valid { return "", xiaomi.ErrNotSupported }
return d.SubProp(0, 0, func(did string, prop *xiaomi.PropertyValue) {
switch {
case d.propOccupancy.valid && prop.SIID == d.propOccupancy.siid && prop.PIID == d.propOccupancy.piid:
if b, ok := prop.Value.(bool); ok { d.stateCache[0] = b }
d.propsCache[0] = toBool(prop.Value)
case d.propNoMotion.valid && prop.SIID == d.propNoMotion.siid && prop.PIID == d.propNoMotion.piid:
d.propsCache[0] = toFloat(prop.Value) == 0
case d.propIlluminance.valid && prop.SIID == d.propIlluminance.siid && prop.PIID == d.propIlluminance.piid:
d.stateCache[1] = toFloat(prop.Value)
d.propsCache[1] = toFloat(prop.Value)
}
occ, _ := d.stateCache[0].(bool)
lux, _ := d.stateCache[1].(float64)
occ, _ := d.propsCache[0].(bool)
lux, _ := d.propsCache[1].(float64)
handler(occ, lux)
})
}
func (d *occupancyDevice) GetState() (bool, float64) {
b, _ := d.stateCache[0].(bool)
f, _ := d.stateCache[1].(float64)
func (d *occupancyDevice) GetProps() (bool, float64) {
b, _ := d.propsCache[0].(bool)
f, _ := d.propsCache[1].(float64)
return b, f
}
func (d *occupancyDevice) FetchState(ctx context.Context) (bool, float64, error) {
func (d *occupancyDevice) FetchProps(ctx context.Context) (bool, float64, error) {
keys := []xiaomi.PropKey{}
if d.propOccupancy.valid { keys = append(keys, xiaomi.PropKey{DID: d.info.DID, SIID: d.propOccupancy.siid, PIID: d.propOccupancy.piid}) }
p := d.occupiedProp()
if p.valid { keys = append(keys, xiaomi.PropKey{DID: d.info.DID, SIID: p.siid, PIID: p.piid}) }
if d.propIlluminance.valid { keys = append(keys, xiaomi.PropKey{DID: d.info.DID, SIID: d.propIlluminance.siid, PIID: d.propIlluminance.piid}) }
props, err := d.client.GetProps(ctx, keys)
if err != nil {
b, _ := d.stateCache[0].(bool)
f, _ := d.stateCache[1].(float64)
b, _ := d.propsCache[0].(bool)
f, _ := d.propsCache[1].(float64)
return b, f, err
}
var occupied bool
var illuminance float64
for _, p := range props {
for _, r := range props {
switch {
case d.propOccupancy.valid && p.SIID == d.propOccupancy.siid && p.PIID == d.propOccupancy.piid:
occupied, _ = p.Value.(bool)
case d.propIlluminance.valid && p.SIID == d.propIlluminance.siid && p.PIID == d.propIlluminance.piid:
illuminance = toFloat(p.Value)
case p.valid && r.SIID == p.siid && r.PIID == p.piid:
occupied = d.occupied(r.Value)
case d.propIlluminance.valid && r.SIID == d.propIlluminance.siid && r.PIID == d.propIlluminance.piid:
illuminance = toFloat(r.Value)
}
}
d.stateCache = [2]interface{}{occupied, illuminance}
d.propsCache = [2]interface{}{occupied, illuminance}
return occupied, illuminance, nil
}
+22 -1
View File
@@ -1,7 +1,28 @@
package devices
// ── 设备类型常量 ──
// 用于 bridge.OnPropsChanged 的 deviceType 字段 / factory 分类
const (
TypeAirConditioner = "air_conditioner" // 空调
TypeLight = "light" // 灯
TypeSwitch = "switch" // 开关
TypeFan = "fan" // 风扇
TypeCover = "cover" // 窗帘/遮阳帘
TypeHumidifier = "humidifier" // 加湿器
TypeVacuum = "vacuum" // 吸尘器/扫地机器人
TypeWaterHeater = "water_heater" // 热水器
TypeThermostat = "thermostat" // 温控器
TypeOccupancySensor = "sensor_occupy" // 人体传感器
TypeTempHumidSensor = "sensor" // 温湿度传感器(官方 URN type)
TypeSpeaker = "speaker" // 音箱
TypeGateway = "gateway" // 网关
TypePlug = "plug" // 插座
)
// ── 设备属性名常量 ──
// 用于 OnStateChanged 回调的 changed 字段,switch 前先用 deviceType 区分设备类型。
// 用于 OnPropsChanged 回调的 changed 字段,switch 前先用 deviceType 区分设备类型。
const (
// ── 通用 ──
+30 -30
View File
@@ -49,13 +49,13 @@ type Speaker interface {
ExecuteTextDirective(ctx context.Context, text string, silent bool) error
PlayMusic(ctx context.Context) error
OnStateChanged(handler func(state SpeakerState, changed string)) (string, error)
GetState() SpeakerState
FetchState(ctx context.Context) (SpeakerState, error)
OnPropsChanged(handler func(props SpeakerProps, changed string)) (string, error)
GetProps() SpeakerProps
FetchProps(ctx context.Context) (SpeakerProps, error)
}
// SpeakerState 音箱完整状态(字段名对齐 SPEC type)
type SpeakerState struct {
// SpeakerProps 音箱完整状态(字段名对齐 SPEC type)
type SpeakerProps struct {
On bool `json:"on"` // SPEC: "on"
Volume int `json:"volume"` // SPEC: "volume"
Mute bool `json:"mute"` // SPEC: "mute"
@@ -86,7 +86,7 @@ type speakerDevice struct {
actExecuteTextDirective isAction
actPlayMusic isAction
stateCache SpeakerState
propsCache SpeakerProps
}
// NewSpeaker creates a Speaker control for the given device.
@@ -183,7 +183,7 @@ func (d *speakerDevice) TurnOff(ctx context.Context) error {
}
func (d *speakerDevice) Toggle(ctx context.Context) error {
s := d.GetState()
s := d.GetProps()
if s.On {
return d.TurnOff(ctx)
}
@@ -339,24 +339,24 @@ func (d *speakerDevice) PlayMusic(ctx context.Context) error {
// ── 订阅 / 状态 ──
func (d *speakerDevice) OnStateChanged(handler func(state SpeakerState, changed string)) (string, error) {
func (d *speakerDevice) OnPropsChanged(handler func(props SpeakerProps, changed string)) (string, error) {
type field struct {
siid, piid int
name string
fill func(*SpeakerState, interface{})
fill func(*SpeakerProps, interface{})
}
var fields []field
add := func(sp siidPiid, name string, fill func(*SpeakerState, interface{})) {
add := func(sp siidPiid, name string, fill func(*SpeakerProps, interface{})) {
if sp.valid {
fields = append(fields, field{sp.siid, sp.piid, name, fill})
}
}
add(d.propOn, "on", func(s *SpeakerState, v interface{}) { s.On, _ = v.(bool) })
add(d.propVolume, "volume", func(s *SpeakerState, v interface{}) { s.Volume = toInt(v) })
add(d.propMute, "mute", func(s *SpeakerState, v interface{}) { s.Mute, _ = v.(bool) })
add(d.propPlayState, "play-state", func(s *SpeakerState, v interface{}) { s.PlayState = toInt(v) })
add(d.propSleepMode, "sleep-mode", func(s *SpeakerState, v interface{}) { s.SleepMode, _ = v.(bool) })
add(d.propAudioID, "audio-id", func(s *SpeakerState, v interface{}) {
add(d.propOn, "on", func(s *SpeakerProps, v interface{}) { s.On, _ = v.(bool) })
add(d.propVolume, "volume", func(s *SpeakerProps, v interface{}) { s.Volume = toInt(v) })
add(d.propMute, "mute", func(s *SpeakerProps, v interface{}) { s.Mute, _ = v.(bool) })
add(d.propPlayState, "play-state", func(s *SpeakerProps, v interface{}) { s.PlayState = toInt(v) })
add(d.propSleepMode, "sleep-mode", func(s *SpeakerProps, v interface{}) { s.SleepMode, _ = v.(bool) })
add(d.propAudioID, "audio-id", func(s *SpeakerProps, v interface{}) {
if str, ok := v.(string); ok {
s.AudioID = str
}
@@ -368,17 +368,17 @@ func (d *speakerDevice) OnStateChanged(handler func(state SpeakerState, changed
return d.SubProp(0, 0, func(did string, prop *xiaomi.PropertyValue) {
for _, f := range fields {
if f.siid == prop.SIID && f.piid == prop.PIID {
f.fill(&d.stateCache, prop.Value)
handler(d.stateCache, f.name)
f.fill(&d.propsCache, prop.Value)
handler(d.propsCache, f.name)
return
}
}
})
}
func (d *speakerDevice) GetState() SpeakerState { return d.stateCache }
func (d *speakerDevice) GetProps() SpeakerProps { return d.propsCache }
func (d *speakerDevice) FetchState(ctx context.Context) (SpeakerState, error) {
func (d *speakerDevice) FetchProps(ctx context.Context) (SpeakerProps, error) {
keys := []xiaomi.PropKey{}
add := func(sp siidPiid) {
if sp.valid {
@@ -394,29 +394,29 @@ func (d *speakerDevice) FetchState(ctx context.Context) (SpeakerState, error) {
props, err := d.client.GetProps(ctx, keys)
if err != nil {
return d.stateCache, err
return d.propsCache, err
}
state := SpeakerState{}
result := SpeakerProps{}
for _, p := range props {
switch {
case match(d.propOn, p):
state.On, _ = p.Value.(bool)
result.On, _ = p.Value.(bool)
case match(d.propVolume, p):
state.Volume = toInt(p.Value)
result.Volume = toInt(p.Value)
case match(d.propMute, p):
state.Mute, _ = p.Value.(bool)
result.Mute, _ = p.Value.(bool)
case match(d.propPlayState, p):
state.PlayState = toInt(p.Value)
result.PlayState = toInt(p.Value)
case match(d.propSleepMode, p):
state.SleepMode, _ = p.Value.(bool)
result.SleepMode, _ = p.Value.(bool)
case match(d.propAudioID, p):
if s, ok := p.Value.(string); ok {
state.AudioID = s
result.AudioID = s
}
}
}
d.stateCache = state
return state, nil
d.propsCache = result
return result, nil
}
func match(sp siidPiid, p *xiaomi.PropertyValue) bool {
+27 -29
View File
@@ -23,16 +23,22 @@ type Switch interface {
ToggleChannel(ctx context.Context, ch int) error
IsChannelOn(ctx context.Context, ch int) (bool, error)
OnStateChanged(handler func(ch int, on bool)) (string, error)
GetState() []bool
FetchState(ctx context.Context) ([]bool, error)
OnPropsChanged(handler func(ch int, on bool)) (string, error)
GetProps() SwitchProps
FetchProps(ctx context.Context) (SwitchProps, error)
}
// SwitchProps 多键开关完整状态。
// JSON: {"on":[true,false,true]},索引 0 = 通道 1
type SwitchProps struct {
On []bool `json:"on"`
}
// switchDevice is the concrete Switch implementation.
type switchDevice struct {
BaseDevice
channels []siidPiid // "on" property per channel (index 0 = channel 1)
stateCache []bool // MQTT 推送维护的本地缓存,索引 0 = 通道 1
propsCache SwitchProps
}
type siidPiid struct {
@@ -46,7 +52,6 @@ func NewSwitch(client *xiaomi.Client, info *xiaomi.DeviceInfo) (Switch, error) {
BaseDevice: NewBaseDevice(client, info),
}
// Use the full SPEC to find all "on" properties (supports multi-channel)
miotDev := d.getMIoTDevice()
if miotDev == nil || miotDev.SpecInstance() == nil {
return nil, xiaomi.ErrNoSPEC
@@ -66,7 +71,7 @@ func NewSwitch(client *xiaomi.Client, info *xiaomi.DeviceInfo) (Switch, error) {
return nil, xiaomi.ErrNotSupported
}
d.channels = channels
d.stateCache = make([]bool, len(channels))
d.propsCache.On = make([]bool, len(channels))
return d, nil
}
@@ -75,18 +80,9 @@ func (d *switchDevice) ChannelCount() int {
return len(d.channels)
}
func (d *switchDevice) TurnOn(ctx context.Context) error {
return d.TurnOnChannel(ctx, 1)
}
func (d *switchDevice) TurnOff(ctx context.Context) error {
return d.TurnOffChannel(ctx, 1)
}
func (d *switchDevice) Toggle(ctx context.Context) error {
return d.ToggleChannel(ctx, 1)
}
func (d *switchDevice) TurnOn(ctx context.Context) error { return d.TurnOnChannel(ctx, 1) }
func (d *switchDevice) TurnOff(ctx context.Context) error { return d.TurnOffChannel(ctx, 1) }
func (d *switchDevice) Toggle(ctx context.Context) error { return d.ToggleChannel(ctx, 1) }
func (d *switchDevice) IsOn(ctx context.Context) (bool, error) {
return d.IsChannelOn(ctx, 1)
}
@@ -133,13 +129,12 @@ func (d *switchDevice) IsChannelOn(ctx context.Context, ch int) (bool, error) {
return false, nil
}
func (d *switchDevice) OnStateChanged(handler func(ch int, on bool)) (string, error) {
func (d *switchDevice) OnPropsChanged(handler func(ch int, on bool)) (string, error) {
if len(d.channels) == 0 {
return "", xiaomi.ErrNotSupported
}
// 构建 siid/piid → channel index 的快速查找表
type chField struct{ siid, piid int }
lookup := make(map[chField]int) // key → channel index (0-based)
lookup := make(map[chField]int)
for i, c := range d.channels {
lookup[chField{c.siid, c.piid}] = i
}
@@ -147,8 +142,8 @@ func (d *switchDevice) OnStateChanged(handler func(ch int, on bool)) (string, er
for k, idx := range lookup {
if k.siid == prop.SIID && k.piid == prop.PIID {
if b, ok := prop.Value.(bool); ok {
d.stateCache[idx] = b
handler(idx+1, b) // ch = idx+1
d.propsCache.On[idx] = b
handler(idx+1, b)
}
return
}
@@ -157,21 +152,24 @@ func (d *switchDevice) OnStateChanged(handler func(ch int, on bool)) (string, er
}
func (d *switchDevice) channel(ch int) (siidPiid, bool) {
i := ch - 1 // channel 1 → index 0
i := ch - 1
if i < 0 || i >= len(d.channels) {
return siidPiid{}, false
}
return d.channels[i], true
}
func (d *switchDevice) GetState() []bool { return d.stateCache }
func (d *switchDevice) FetchState(ctx context.Context) ([]bool, error) {
func (d *switchDevice) GetProps() SwitchProps { return d.propsCache }
func (d *switchDevice) FetchProps(ctx context.Context) (SwitchProps, error) {
keys := make([]xiaomi.PropKey, len(d.channels))
for i, c := range d.channels {
keys[i] = xiaomi.PropKey{DID: d.info.DID, SIID: c.siid, PIID: c.piid}
}
props, err := d.client.GetProps(ctx, keys)
if err != nil { return d.stateCache, err }
if err != nil {
return d.propsCache, err
}
states := make([]bool, len(d.channels))
for i, c := range d.channels {
for _, p := range props {
@@ -181,6 +179,6 @@ func (d *switchDevice) FetchState(ctx context.Context) ([]bool, error) {
}
}
}
d.stateCache = states
return states, nil
d.propsCache.On = states
return d.propsCache, nil
}
+13 -13
View File
@@ -13,16 +13,16 @@ type TempHumiditySensor interface {
GetTemperature(ctx context.Context) (float64, error)
GetHumidity(ctx context.Context) (float64, error)
OnStateChanged(handler func(temp, humidity float64)) (string, error)
GetState() (float64, float64)
FetchState(ctx context.Context) (float64, float64, error)
OnPropsChanged(handler func(temp, humidity float64)) (string, error)
GetProps() (float64, float64)
FetchProps(ctx context.Context) (float64, float64, error)
}
type tempHumidityDevice struct {
BaseDevice
propTemp siidPiid
propHumidity siidPiid
stateCache [2]float64 // [0]=temperature, [1]=humidity
propsCache [2]float64 // [0]=temperature, [1]=humidity
}
func NewTempHumiditySensor(client *xiaomi.Client, info *xiaomi.DeviceInfo) (TempHumiditySensor, error) {
@@ -61,33 +61,33 @@ func (d *tempHumidityDevice) GetHumidity(ctx context.Context) (float64, error) {
return toFloat(prop.Value), nil
}
func (d *tempHumidityDevice) OnStateChanged(handler func(temp, humidity float64)) (string, error) {
func (d *tempHumidityDevice) OnPropsChanged(handler func(temp, humidity float64)) (string, error) {
if !d.propTemp.valid && !d.propHumidity.valid { return "", xiaomi.ErrNotSupported }
return d.SubProp(0, 0, func(did string, prop *xiaomi.PropertyValue) {
switch {
case d.propTemp.valid && prop.SIID == d.propTemp.siid && prop.PIID == d.propTemp.piid:
d.stateCache[0] = toFloat(prop.Value)
d.propsCache[0] = toFloat(prop.Value)
case d.propHumidity.valid && prop.SIID == d.propHumidity.siid && prop.PIID == d.propHumidity.piid:
d.stateCache[1] = toFloat(prop.Value)
d.propsCache[1] = toFloat(prop.Value)
}
handler(d.stateCache[0], d.stateCache[1])
handler(d.propsCache[0], d.propsCache[1])
})
}
func (d *tempHumidityDevice) GetState() (float64, float64) { return d.stateCache[0], d.stateCache[1] }
func (d *tempHumidityDevice) FetchState(ctx context.Context) (float64, float64, error) {
func (d *tempHumidityDevice) GetProps() (float64, float64) { return d.propsCache[0], d.propsCache[1] }
func (d *tempHumidityDevice) FetchProps(ctx context.Context) (float64, float64, error) {
keys := []xiaomi.PropKey{}
if d.propTemp.valid { keys = append(keys, xiaomi.PropKey{DID: d.info.DID, SIID: d.propTemp.siid, PIID: d.propTemp.piid}) }
if d.propHumidity.valid { keys = append(keys, xiaomi.PropKey{DID: d.info.DID, SIID: d.propHumidity.siid, PIID: d.propHumidity.piid}) }
props, err := d.client.GetProps(ctx, keys)
if err != nil { return d.stateCache[0], d.stateCache[1], err }
state := [2]float64{d.stateCache[0], d.stateCache[1]}
if err != nil { return d.propsCache[0], d.propsCache[1], err }
state := [2]float64{d.propsCache[0], d.propsCache[1]}
for _, p := range props {
switch {
case d.propTemp.valid && p.SIID == d.propTemp.siid && p.PIID == d.propTemp.piid: state[0] = toFloat(p.Value)
case d.propHumidity.valid && p.SIID == d.propHumidity.siid && p.PIID == d.propHumidity.piid: state[1] = toFloat(p.Value)
}
}
d.stateCache = state
d.propsCache = state
return state[0], state[1], nil
}
+20 -20
View File
@@ -17,9 +17,9 @@ type Thermostat interface {
SetHeating(ctx context.Context, on bool) error
IsHeating(ctx context.Context) (bool, error)
OnStateChanged(handler func(state HeaterState, changed string)) (string, error)
GetState() HeaterState
FetchState(ctx context.Context) (HeaterState, error)
OnPropsChanged(handler func(props HeaterProps, changed string)) (string, error)
GetProps() HeaterProps
FetchProps(ctx context.Context) (HeaterProps, error)
}
type thermostatDevice struct {
@@ -28,7 +28,7 @@ type thermostatDevice struct {
propTargetTemp siidPiid
propCurrentTemp siidPiid
propHeating siidPiid
stateCache HeaterState
propsCache HeaterProps
}
func NewThermostat(client *xiaomi.Client, info *xiaomi.DeviceInfo) (Thermostat, error) {
@@ -87,46 +87,46 @@ func (d *thermostatDevice) IsHeating(ctx context.Context) (bool, error) {
return false, nil
}
func (d *thermostatDevice) OnStateChanged(handler func(state HeaterState, changed string)) (string, error) {
type field struct{ siid, piid int; name string; fill func(*HeaterState, interface{}) }
func (d *thermostatDevice) OnPropsChanged(handler func(props HeaterProps, changed string)) (string, error) {
type field struct{ siid, piid int; name string; fill func(*HeaterProps, interface{}) }
var fields []field
if d.propOn.valid {
fields = append(fields, field{d.propOn.siid, d.propOn.piid, "on", func(s *HeaterState, v interface{}) { s.On, _ = v.(bool) }})
fields = append(fields, field{d.propOn.siid, d.propOn.piid, "on", func(s *HeaterProps, v interface{}) { s.On, _ = v.(bool) }})
}
if d.propTargetTemp.valid {
fields = append(fields, field{d.propTargetTemp.siid, d.propTargetTemp.piid, "target-temperature", func(s *HeaterState, v interface{}) { s.TargetTemp = toFloat(v) }})
fields = append(fields, field{d.propTargetTemp.siid, d.propTargetTemp.piid, "target-temperature", func(s *HeaterProps, v interface{}) { s.TargetTemp = toFloat(v) }})
}
if d.propCurrentTemp.valid {
fields = append(fields, field{d.propCurrentTemp.siid, d.propCurrentTemp.piid, "temperature", func(s *HeaterState, v interface{}) { s.CurrentTemp = toFloat(v) }})
fields = append(fields, field{d.propCurrentTemp.siid, d.propCurrentTemp.piid, "temperature", func(s *HeaterProps, v interface{}) { s.CurrentTemp = toFloat(v) }})
}
if len(fields) == 0 { return "", xiaomi.ErrNotSupported }
return d.SubProp(0, 0, func(did string, prop *xiaomi.PropertyValue) {
for _, f := range fields {
if f.siid == prop.SIID && f.piid == prop.PIID {
f.fill(&d.stateCache, prop.Value)
handler(d.stateCache, f.name)
f.fill(&d.propsCache, prop.Value)
handler(d.propsCache, f.name)
return
}
}
})
}
func (d *thermostatDevice) GetState() HeaterState { return d.stateCache }
func (d *thermostatDevice) FetchState(ctx context.Context) (HeaterState, error) {
func (d *thermostatDevice) GetProps() HeaterProps { return d.propsCache }
func (d *thermostatDevice) FetchProps(ctx context.Context) (HeaterProps, error) {
keys := []xiaomi.PropKey{}
if d.propOn.valid { keys = append(keys, xiaomi.PropKey{DID: d.info.DID, SIID: d.propOn.siid, PIID: d.propOn.piid}) }
if d.propTargetTemp.valid { keys = append(keys, xiaomi.PropKey{DID: d.info.DID, SIID: d.propTargetTemp.siid, PIID: d.propTargetTemp.piid}) }
if d.propCurrentTemp.valid { keys = append(keys, xiaomi.PropKey{DID: d.info.DID, SIID: d.propCurrentTemp.siid, PIID: d.propCurrentTemp.piid}) }
props, err := d.client.GetProps(ctx, keys)
if err != nil { return d.stateCache, err }
state := HeaterState{}
if err != nil { return d.propsCache, err }
result := HeaterProps{}
for _, p := range props {
switch {
case d.propOn.valid && p.SIID == d.propOn.siid && p.PIID == d.propOn.piid: state.On, _ = p.Value.(bool)
case d.propTargetTemp.valid && p.SIID == d.propTargetTemp.siid && p.PIID == d.propTargetTemp.piid: state.TargetTemp = toFloat(p.Value)
case d.propCurrentTemp.valid && p.SIID == d.propCurrentTemp.siid && p.PIID == d.propCurrentTemp.piid: state.CurrentTemp = toFloat(p.Value)
case d.propOn.valid && p.SIID == d.propOn.siid && p.PIID == d.propOn.piid: result.On, _ = p.Value.(bool)
case d.propTargetTemp.valid && p.SIID == d.propTargetTemp.siid && p.PIID == d.propTargetTemp.piid: result.TargetTemp = toFloat(p.Value)
case d.propCurrentTemp.valid && p.SIID == d.propCurrentTemp.siid && p.PIID == d.propCurrentTemp.piid: result.CurrentTemp = toFloat(p.Value)
}
}
d.stateCache = state
return state, nil
d.propsCache = result
return result, nil
}
+22 -22
View File
@@ -13,15 +13,15 @@ type Vacuum interface {
ReturnToDock(ctx context.Context) error
SetFanLevel(ctx context.Context, level int) error
GetFanLevel(ctx context.Context) (int, error)
GetStatus(ctx context.Context) (VacuumStatus, error)
GetStatus(ctx context.Context) (VacuumProps, error)
OnStateChanged(handler func(state VacuumStatus, changed string)) (string, error)
GetState() VacuumStatus
FetchState(ctx context.Context) (VacuumStatus, error)
OnPropsChanged(handler func(props VacuumProps, changed string)) (string, error)
GetProps() VacuumProps
FetchProps(ctx context.Context) (VacuumProps, error)
}
// VacuumStatus 扫地机运行状态(字段名对齐 SPEC type)
type VacuumStatus struct {
// VacuumProps 扫地机运行状态(字段名对齐 SPEC type)
type VacuumProps struct {
On bool `json:"on"`
Mode int `json:"mode"`
FanLevel int `json:"fanLevel"`
@@ -34,7 +34,7 @@ type vacuumDevice struct {
propMode siidPiid
propFanLevel siidPiid
propBattery siidPiid
stateCache VacuumStatus
propsCache VacuumProps
}
func NewVacuum(client *xiaomi.Client, info *xiaomi.DeviceInfo) (Vacuum, error) {
@@ -72,7 +72,7 @@ func (d *vacuumDevice) GetFanLevel(ctx context.Context) (int, error) {
if err != nil { return 0, err }
return toInt(prop.Value), nil
}
func (d *vacuumDevice) GetStatus(ctx context.Context) (VacuumStatus, error) {
func (d *vacuumDevice) GetStatus(ctx context.Context) (VacuumProps, error) {
// 云端实时查询
keys := []xiaomi.PropKey{}
add := func(sp siidPiid) {
@@ -80,8 +80,8 @@ func (d *vacuumDevice) GetStatus(ctx context.Context) (VacuumStatus, error) {
}
add(d.propOn); add(d.propMode); add(d.propFanLevel); add(d.propBattery)
props, err := d.client.GetProps(ctx, keys)
if err != nil { return VacuumStatus{}, err }
status := VacuumStatus{}
if err != nil { return VacuumProps{}, err }
status := VacuumProps{}
for _, p := range props {
switch {
case d.propOn.valid && p.SIID == d.propOn.siid && p.PIID == d.propOn.piid:
@@ -97,37 +97,37 @@ func (d *vacuumDevice) GetStatus(ctx context.Context) (VacuumStatus, error) {
return status, nil
}
func (d *vacuumDevice) OnStateChanged(handler func(state VacuumStatus, changed string)) (string, error) {
type field struct{ siid, piid int; name string; fill func(*VacuumStatus, interface{}) }
func (d *vacuumDevice) OnPropsChanged(handler func(props VacuumProps, changed string)) (string, error) {
type field struct{ siid, piid int; name string; fill func(*VacuumProps, interface{}) }
var fields []field
if d.propOn.valid {
fields = append(fields, field{d.propOn.siid, d.propOn.piid, "on", func(s *VacuumStatus, v interface{}) { s.On, _ = v.(bool) }})
fields = append(fields, field{d.propOn.siid, d.propOn.piid, "on", func(s *VacuumProps, v interface{}) { s.On, _ = v.(bool) }})
}
if d.propMode.valid {
fields = append(fields, field{d.propMode.siid, d.propMode.piid, "mode", func(s *VacuumStatus, v interface{}) { s.Mode = toInt(v) }})
fields = append(fields, field{d.propMode.siid, d.propMode.piid, "mode", func(s *VacuumProps, v interface{}) { s.Mode = toInt(v) }})
}
if d.propFanLevel.valid {
fields = append(fields, field{d.propFanLevel.siid, d.propFanLevel.piid, "fan-level", func(s *VacuumStatus, v interface{}) { s.FanLevel = toInt(v) }})
fields = append(fields, field{d.propFanLevel.siid, d.propFanLevel.piid, "fan-level", func(s *VacuumProps, v interface{}) { s.FanLevel = toInt(v) }})
}
if d.propBattery.valid {
fields = append(fields, field{d.propBattery.siid, d.propBattery.piid, "battery", func(s *VacuumStatus, v interface{}) { s.Battery = toInt(v) }})
fields = append(fields, field{d.propBattery.siid, d.propBattery.piid, "battery", func(s *VacuumProps, v interface{}) { s.Battery = toInt(v) }})
}
if len(fields) == 0 { return "", xiaomi.ErrNotSupported }
return d.SubProp(0, 0, func(did string, prop *xiaomi.PropertyValue) {
for _, f := range fields {
if f.siid == prop.SIID && f.piid == prop.PIID {
f.fill(&d.stateCache, prop.Value)
handler(d.stateCache, f.name)
f.fill(&d.propsCache, prop.Value)
handler(d.propsCache, f.name)
return
}
}
})
}
func (d *vacuumDevice) GetState() VacuumStatus { return d.stateCache }
func (d *vacuumDevice) FetchState(ctx context.Context) (VacuumStatus, error) {
func (d *vacuumDevice) GetProps() VacuumProps { return d.propsCache }
func (d *vacuumDevice) FetchProps(ctx context.Context) (VacuumProps, error) {
status, err := d.GetStatus(ctx)
if err != nil { return d.stateCache, err }
d.stateCache = status
if err != nil { return d.propsCache, err }
d.propsCache = status
return status, nil
}
+22 -22
View File
@@ -15,13 +15,13 @@ type WaterHeater interface {
GetTargetTemp(ctx context.Context) (float64, error)
GetCurrentTemp(ctx context.Context) (float64, error)
OnStateChanged(handler func(state HeaterState, changed string)) (string, error)
GetState() HeaterState
FetchState(ctx context.Context) (HeaterState, error)
OnPropsChanged(handler func(props HeaterProps, changed string)) (string, error)
GetProps() HeaterProps
FetchProps(ctx context.Context) (HeaterProps, error)
}
// HeaterState 热水器状态(MQTT 缓存)
type HeaterState struct {
// HeaterProps 热水器状态(MQTT 缓存)
type HeaterProps struct {
On bool `json:"on"`
TargetTemp float64 `json:"targetTemperature"`
CurrentTemp float64 `json:"temperature"`
@@ -32,7 +32,7 @@ type waterHeaterDevice struct {
propOn siidPiid
propTargetTemp siidPiid
propCurrentTemp siidPiid
stateCache HeaterState
propsCache HeaterProps
}
func NewWaterHeater(client *xiaomi.Client, info *xiaomi.DeviceInfo) (WaterHeater, error) {
@@ -79,46 +79,46 @@ func (d *waterHeaterDevice) GetCurrentTemp(ctx context.Context) (float64, error)
return toFloat(prop.Value), nil
}
func (d *waterHeaterDevice) OnStateChanged(handler func(state HeaterState, changed string)) (string, error) {
type field struct{ siid, piid int; name string; fill func(*HeaterState, interface{}) }
func (d *waterHeaterDevice) OnPropsChanged(handler func(props HeaterProps, changed string)) (string, error) {
type field struct{ siid, piid int; name string; fill func(*HeaterProps, interface{}) }
var fields []field
if d.propOn.valid {
fields = append(fields, field{d.propOn.siid, d.propOn.piid, "on", func(s *HeaterState, v interface{}) { s.On, _ = v.(bool) }})
fields = append(fields, field{d.propOn.siid, d.propOn.piid, "on", func(s *HeaterProps, v interface{}) { s.On, _ = v.(bool) }})
}
if d.propTargetTemp.valid {
fields = append(fields, field{d.propTargetTemp.siid, d.propTargetTemp.piid, "target-temperature", func(s *HeaterState, v interface{}) { s.TargetTemp = toFloat(v) }})
fields = append(fields, field{d.propTargetTemp.siid, d.propTargetTemp.piid, "target-temperature", func(s *HeaterProps, v interface{}) { s.TargetTemp = toFloat(v) }})
}
if d.propCurrentTemp.valid {
fields = append(fields, field{d.propCurrentTemp.siid, d.propCurrentTemp.piid, "temperature", func(s *HeaterState, v interface{}) { s.CurrentTemp = toFloat(v) }})
fields = append(fields, field{d.propCurrentTemp.siid, d.propCurrentTemp.piid, "temperature", func(s *HeaterProps, v interface{}) { s.CurrentTemp = toFloat(v) }})
}
if len(fields) == 0 { return "", xiaomi.ErrNotSupported }
return d.SubProp(0, 0, func(did string, prop *xiaomi.PropertyValue) {
for _, f := range fields {
if f.siid == prop.SIID && f.piid == prop.PIID {
f.fill(&d.stateCache, prop.Value)
handler(d.stateCache, f.name)
f.fill(&d.propsCache, prop.Value)
handler(d.propsCache, f.name)
return
}
}
})
}
func (d *waterHeaterDevice) GetState() HeaterState { return d.stateCache }
func (d *waterHeaterDevice) FetchState(ctx context.Context) (HeaterState, error) {
func (d *waterHeaterDevice) GetProps() HeaterProps { return d.propsCache }
func (d *waterHeaterDevice) FetchProps(ctx context.Context) (HeaterProps, error) {
keys := []xiaomi.PropKey{}
if d.propOn.valid { keys = append(keys, xiaomi.PropKey{DID: d.info.DID, SIID: d.propOn.siid, PIID: d.propOn.piid}) }
if d.propTargetTemp.valid { keys = append(keys, xiaomi.PropKey{DID: d.info.DID, SIID: d.propTargetTemp.siid, PIID: d.propTargetTemp.piid}) }
if d.propCurrentTemp.valid { keys = append(keys, xiaomi.PropKey{DID: d.info.DID, SIID: d.propCurrentTemp.siid, PIID: d.propCurrentTemp.piid}) }
props, err := d.client.GetProps(ctx, keys)
if err != nil { return d.stateCache, err }
state := HeaterState{}
if err != nil { return d.propsCache, err }
result := HeaterProps{}
for _, p := range props {
switch {
case d.propOn.valid && p.SIID == d.propOn.siid && p.PIID == d.propOn.piid: state.On, _ = p.Value.(bool)
case d.propTargetTemp.valid && p.SIID == d.propTargetTemp.siid && p.PIID == d.propTargetTemp.piid: state.TargetTemp = toFloat(p.Value)
case d.propCurrentTemp.valid && p.SIID == d.propCurrentTemp.siid && p.PIID == d.propCurrentTemp.piid: state.CurrentTemp = toFloat(p.Value)
case d.propOn.valid && p.SIID == d.propOn.siid && p.PIID == d.propOn.piid: result.On, _ = p.Value.(bool)
case d.propTargetTemp.valid && p.SIID == d.propTargetTemp.siid && p.PIID == d.propTargetTemp.piid: result.TargetTemp = toFloat(p.Value)
case d.propCurrentTemp.valid && p.SIID == d.propCurrentTemp.siid && p.PIID == d.propCurrentTemp.piid: result.CurrentTemp = toFloat(p.Value)
}
}
d.stateCache = state
return state, nil
d.propsCache = result
return result, nil
}