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