**AC 驱动增强** - 支持 thermostat + air-conditioner + air-fresh 多服务三合一设备,属性跨服务合并解析 - 新增 specResolver.findValListInService/findInService/hasService/serviceShortName 方法 - 移除 KindThermostat,heater/electric-blanket/thermostat 重新归类为 KindAirConditioner **Cover 窗帘重构** - CoverProps 结构化:Position/Status/Fault 分离 - SetPosition 优先写 target-position,回退 current-position - OnPropsChanged/FetchProps 支持多属性同步刷新 **新增设备类型** - Plug 驱动:插座/插头,支持开合、在线状态、属性变更订阅 - Gateway 驱动:网关设备类型识别 - miot/miot_errors.go:新增 MIoT 云端错误码定义 **MIoT 客户端健壮性** - SetProp 空结果视为失败;设备被移除或离线时自动刷新设备列表 - GetDevices 的 has_more 改用 parseBool 严格解析 - MIPS SubscribeMany 批量订阅减少往返 - matchURN 改为按段精确匹配,避免 tofan 误匹配 fan **Bridge** - 移除 Thermostat case,Cover 改用结构化 CoverProps - 新增设备创建后自动订阅属性变更 **其他** - 日志整体 Info → Debug 降噪 - .gitignore 忽略 /.codebuddy/memory - 更新 devices_test 与 08_adv_filter_classify 示例
670 lines
20 KiB
Go
670 lines
20 KiB
Go
package devices
|
||
|
||
import (
|
||
"context"
|
||
"strings"
|
||
|
||
"xiaomihome/miot"
|
||
"xiaomihome/xiaomi"
|
||
)
|
||
|
||
// valMapper maps between device raw values and semantic names. Built from SPEC ValueList.
|
||
// Different brands assign different raw values for the same mode name,
|
||
// so this must be per-device. Names are lowercased from SPEC description for consistency.
|
||
type valMapper struct {
|
||
val2name map[int]string
|
||
name2val map[string]int
|
||
names []string
|
||
}
|
||
|
||
func newValMapper(vl miot.MIoTSpecValueList) *valMapper {
|
||
vm := &valMapper{
|
||
val2name: make(map[int]string),
|
||
name2val: make(map[string]int),
|
||
}
|
||
for _, item := range vl {
|
||
v := toInt(item.Value)
|
||
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
|
||
}
|
||
|
||
func (vm *valMapper) name(v int) string {
|
||
if vm == nil { return "" }
|
||
return vm.val2name[v]
|
||
}
|
||
|
||
func (vm *valMapper) val(name string) (int, bool) {
|
||
if vm == nil { return 0, false }
|
||
v, ok := vm.name2val[strings.ToLower(name)]
|
||
return v, ok
|
||
}
|
||
|
||
func (vm *valMapper) allNames() []string {
|
||
if vm == nil { return nil }
|
||
return vm.names
|
||
}
|
||
|
||
// ========== 空调模式/风速/摆风常量 ==========
|
||
|
||
const (
|
||
ACModeCool = "cool"
|
||
ACModeHeat = "heat"
|
||
ACModeFan = "fan"
|
||
ACModeDry = "dry"
|
||
ACModeAuto = "auto"
|
||
|
||
ACFanLow = "low"
|
||
ACFanMedium = "medium"
|
||
ACFanHigh = "high"
|
||
ACFanAuto = "auto"
|
||
|
||
ACSwingOff = "off"
|
||
ACSwingVertical = "vertical"
|
||
ACSwingHorizontal = "horizontal"
|
||
ACSwingBoth = "both"
|
||
)
|
||
|
||
// ========== AirConditioner 接口 ==========
|
||
|
||
// AirConditioner 空调统一控制接口
|
||
type AirConditioner interface {
|
||
Device
|
||
TurnOn(ctx context.Context) error
|
||
TurnOff(ctx context.Context) error
|
||
IsOn(ctx context.Context) (bool, error)
|
||
|
||
// 能力查询
|
||
HasFanSpeed() bool
|
||
HasSwing() bool
|
||
HasHumidity() bool
|
||
HasAuxHeat() bool
|
||
HasEco() bool
|
||
|
||
// 模式控制
|
||
SetMode(ctx context.Context, mode string) error
|
||
GetMode(ctx context.Context) (string, error)
|
||
|
||
// 温度控制
|
||
SetTargetTemp(ctx context.Context, celsius float64) error
|
||
GetTargetTemp(ctx context.Context) (float64, error)
|
||
GetCurrentTemp(ctx context.Context) (float64, error) // 室内温度
|
||
|
||
// 风速控制
|
||
SetFanSpeed(ctx context.Context, speed string) error
|
||
GetFanSpeed(ctx context.Context) (string, error)
|
||
|
||
// 摆风控制
|
||
SetSwing(ctx context.Context, mode string) error
|
||
GetSwing(ctx context.Context) (string, error)
|
||
|
||
// 湿度
|
||
SetTargetHumidity(ctx context.Context, pct int) error
|
||
GetTargetHumidity(ctx context.Context) (int, error)
|
||
GetCurrentHumidity(ctx context.Context) (int, error)
|
||
|
||
// 电辅热
|
||
SetAuxHeat(ctx context.Context, on bool) error
|
||
GetAuxHeat(ctx context.Context) (bool, error)
|
||
|
||
// 节能
|
||
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)
|
||
|
||
// 订阅
|
||
OnPropsChanged(handler func(props ACProps, changed string)) (string, error)
|
||
GetProps() ACProps
|
||
FetchProps(ctx context.Context) (ACProps, error)
|
||
}
|
||
|
||
// 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"`
|
||
Temperature float64 `json:"temperature"`
|
||
TargetHumidity int `json:"targetHumidity"`
|
||
CurrentHumidity int `json:"currentHumidity"`
|
||
FanLevel string `json:"fanLevel"` // SPEC ValueList 解析后的名称:low/medium/high/auto
|
||
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 {
|
||
BaseDevice
|
||
propOn siidPiid // "on"
|
||
propMode siidPiid // "mode"
|
||
propTargetTemp siidPiid // "target-temperature"
|
||
propCurrentTemp siidPiid // "temperature"
|
||
propFanSpeed siidPiid // "fan-level"
|
||
propSwing siidPiid // "swing-mode" or "horizontal-swing" or "vertical-swing"
|
||
propTargetHumidity siidPiid // "target-humidity"
|
||
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
|
||
|
||
propsCache ACProps // MQTT 推送维护的本地缓存
|
||
}
|
||
|
||
// NewAirConditioner creates an AirConditioner control for the given device.
|
||
func NewAirConditioner(client *xiaomi.Client, info *xiaomi.DeviceInfo) (AirConditioner, error) {
|
||
d := &acDevice{
|
||
BaseDevice: NewBaseDevice(client, info),
|
||
}
|
||
|
||
dev := d.getMIoTDevice()
|
||
if dev == nil {
|
||
return nil, xiaomi.ErrNoSPEC
|
||
}
|
||
spec := dev.SpecInstance()
|
||
if spec == nil {
|
||
return nil, xiaomi.ErrNoSPEC
|
||
}
|
||
|
||
resolver := d.getSpecResolver()
|
||
if resolver == nil {
|
||
return nil, xiaomi.ErrNoSPEC
|
||
}
|
||
|
||
// Helper: find property ValueList from spec by type name
|
||
findValList := func(propType string) miot.MIoTSpecValueList {
|
||
for _, svc := range spec.Services {
|
||
for _, p := range svc.Properties {
|
||
if propertyShortName(p.Type) == propType {
|
||
return p.ValueList
|
||
}
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// findValListInService 从指定服务(短名)内查找属性的 ValueList。
|
||
findValListInService := func(svcName, propType string) miot.MIoTSpecValueList {
|
||
for _, svc := range spec.Services {
|
||
if serviceShortName(svc.Type) != svcName {
|
||
continue
|
||
}
|
||
for _, p := range svc.Properties {
|
||
if propertyShortName(p.Type) == propType {
|
||
return p.ValueList
|
||
}
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// "on" is required。
|
||
// 三合一设备(thermostat+air-conditioner+air-fresh 多服务同名冲突)的空调实体在 thermostat 服务:
|
||
// 存在 thermostat 服务时优先从它取 on/target-temperature,否则取第一个同名属性(普通空调)。
|
||
if resolver.hasService("thermostat") {
|
||
if s, p, ok := resolver.findInService("thermostat", "on"); ok {
|
||
d.propOn = siidPiid{s, p, true}
|
||
} else if s, p, ok := resolver.find("on"); ok {
|
||
d.propOn = siidPiid{s, p, true}
|
||
}
|
||
} else {
|
||
siid, piid, ok := resolver.find("on")
|
||
if !ok {
|
||
return nil, xiaomi.ErrNotSupported
|
||
}
|
||
d.propOn = siidPiid{siid, piid, true}
|
||
}
|
||
|
||
// Optional properties — each independently resolved
|
||
if s, p, ok := resolver.find("mode"); ok {
|
||
d.propMode = siidPiid{s, p, true}
|
||
d.modeMap = newValMapper(findValList("mode"))
|
||
}
|
||
if resolver.hasService("thermostat") {
|
||
// 三合一设备:target-temperature 优先 thermostat 服务(空调温度)
|
||
if s, p, ok := resolver.findInService("thermostat", "target-temperature"); ok {
|
||
d.propTargetTemp = siidPiid{s, p, true}
|
||
} else if s, p, ok := resolver.find("target-temperature"); ok {
|
||
d.propTargetTemp = siidPiid{s, p, true}
|
||
}
|
||
} else if s, p, ok := resolver.find("target-temperature"); ok {
|
||
d.propTargetTemp = siidPiid{s, p, true}
|
||
}
|
||
if s, p, ok := resolver.find("temperature"); ok {
|
||
d.propCurrentTemp = siidPiid{s, p, true}
|
||
}
|
||
// fan-level:三合一设备优先 thermostat 服务(空调风速 siid=4/piid=3),
|
||
// 否则取第一个同名属性(普通空调 air-conditioner 服务 / air-fresh 新风机)。
|
||
if resolver.hasService("thermostat") {
|
||
if s, p, ok := resolver.findInService("thermostat", "fan-level"); ok {
|
||
d.propFanSpeed = siidPiid{s, p, true}
|
||
d.fanMap = newValMapper(findValListInService("thermostat", "fan-level"))
|
||
} else if s, p, ok := resolver.find("fan-level"); ok {
|
||
d.propFanSpeed = siidPiid{s, p, true}
|
||
d.fanMap = newValMapper(findValList("fan-level"))
|
||
}
|
||
} else if s, p, ok := resolver.find("fan-level"); ok {
|
||
d.propFanSpeed = siidPiid{s, p, true}
|
||
d.fanMap = newValMapper(findValList("fan-level"))
|
||
}
|
||
// Try multiple swing mode type names
|
||
if s, p, ok := resolver.find("swing-mode"); ok {
|
||
d.propSwing = siidPiid{s, p, true}
|
||
d.swingMap = newValMapper(findValList("swing-mode"))
|
||
} else if s, p, ok := resolver.find("horizontal-swing"); ok {
|
||
d.propSwing = siidPiid{s, p, true}
|
||
d.swingMap = newValMapper(findValList("horizontal-swing"))
|
||
} else if s, p, ok := resolver.find("vertical-swing"); ok {
|
||
d.propSwing = siidPiid{s, p, true}
|
||
d.swingMap = newValMapper(findValList("vertical-swing"))
|
||
}
|
||
if s, p, ok := resolver.find("target-humidity"); ok {
|
||
d.propTargetHumidity = siidPiid{s, p, true}
|
||
}
|
||
if s, p, ok := resolver.find("relative-humidity"); ok {
|
||
d.propCurrentHumidity = siidPiid{s, p, true}
|
||
}
|
||
if s, p, ok := resolver.find("heater"); ok {
|
||
d.propAuxHeat = siidPiid{s, p, true}
|
||
} else if s, p, ok := resolver.find("aux-heat"); ok {
|
||
d.propAuxHeat = siidPiid{s, p, true}
|
||
}
|
||
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
|
||
}
|
||
|
||
func (d *acDevice) TurnOn(ctx context.Context) error {
|
||
if !d.propOn.valid {
|
||
return xiaomi.ErrNotSupported
|
||
}
|
||
return d.SetProp(ctx, d.propOn.siid, d.propOn.piid, true)
|
||
}
|
||
|
||
func (d *acDevice) TurnOff(ctx context.Context) error {
|
||
if !d.propOn.valid {
|
||
return xiaomi.ErrNotSupported
|
||
}
|
||
return d.SetProp(ctx, d.propOn.siid, d.propOn.piid, false)
|
||
}
|
||
|
||
func (d *acDevice) IsOn(ctx context.Context) (bool, error) {
|
||
if !d.propOn.valid {
|
||
return false, xiaomi.ErrNotSupported
|
||
}
|
||
prop, err := d.GetProp(ctx, d.propOn.siid, d.propOn.piid)
|
||
if err != nil {
|
||
return false, err
|
||
}
|
||
if b, ok := prop.Value.(bool); ok {
|
||
return b, nil
|
||
}
|
||
return false, nil
|
||
}
|
||
|
||
func (d *acDevice) HasFanSpeed() bool { return d.propFanSpeed.valid }
|
||
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 {
|
||
return xiaomi.ErrNotSupported
|
||
}
|
||
v, ok := d.modeMap.val(mode)
|
||
if !ok {
|
||
return xiaomi.ErrNotSupported
|
||
}
|
||
return d.SetProp(ctx, d.propMode.siid, d.propMode.piid, v)
|
||
}
|
||
|
||
func (d *acDevice) GetMode(ctx context.Context) (string, error) {
|
||
if !d.propMode.valid {
|
||
return ACModeAuto, xiaomi.ErrNotSupported
|
||
}
|
||
prop, err := d.GetProp(ctx, d.propMode.siid, d.propMode.piid)
|
||
if err != nil {
|
||
return ACModeAuto, err
|
||
}
|
||
return d.modeMap.name(toInt(prop.Value)), nil
|
||
}
|
||
|
||
func (d *acDevice) SetTargetTemp(ctx context.Context, celsius float64) error {
|
||
if !d.propTargetTemp.valid {
|
||
return xiaomi.ErrNotSupported
|
||
}
|
||
return d.SetProp(ctx, d.propTargetTemp.siid, d.propTargetTemp.piid, celsius)
|
||
}
|
||
|
||
func (d *acDevice) GetTargetTemp(ctx context.Context) (float64, error) {
|
||
if !d.propTargetTemp.valid {
|
||
return 0, xiaomi.ErrNotSupported
|
||
}
|
||
prop, err := d.GetProp(ctx, d.propTargetTemp.siid, d.propTargetTemp.piid)
|
||
if err != nil {
|
||
return 0, err
|
||
}
|
||
return toFloat(prop.Value), nil
|
||
}
|
||
|
||
func (d *acDevice) GetCurrentTemp(ctx context.Context) (float64, error) {
|
||
if !d.propCurrentTemp.valid {
|
||
return 0, xiaomi.ErrNotSupported
|
||
}
|
||
prop, err := d.GetProp(ctx, d.propCurrentTemp.siid, d.propCurrentTemp.piid)
|
||
if err != nil {
|
||
return 0, err
|
||
}
|
||
return toFloat(prop.Value), nil
|
||
}
|
||
|
||
func (d *acDevice) SetFanSpeed(ctx context.Context, speed string) error {
|
||
if !d.propFanSpeed.valid {
|
||
return xiaomi.ErrNotSupported
|
||
}
|
||
v, ok := d.fanMap.val(speed)
|
||
if !ok {
|
||
return xiaomi.ErrNotSupported
|
||
}
|
||
return d.SetProp(ctx, d.propFanSpeed.siid, d.propFanSpeed.piid, v)
|
||
}
|
||
|
||
func (d *acDevice) GetFanSpeed(ctx context.Context) (string, error) {
|
||
if !d.propFanSpeed.valid {
|
||
return ACFanAuto, xiaomi.ErrNotSupported
|
||
}
|
||
prop, err := d.GetProp(ctx, d.propFanSpeed.siid, d.propFanSpeed.piid)
|
||
if err != nil {
|
||
return ACFanAuto, err
|
||
}
|
||
return d.fanMap.name(toInt(prop.Value)), nil
|
||
}
|
||
|
||
func (d *acDevice) SetSwing(ctx context.Context, mode string) error {
|
||
if !d.propSwing.valid {
|
||
return xiaomi.ErrNotSupported
|
||
}
|
||
v, ok := d.swingMap.val(mode)
|
||
if !ok {
|
||
return xiaomi.ErrNotSupported
|
||
}
|
||
return d.SetProp(ctx, d.propSwing.siid, d.propSwing.piid, v)
|
||
}
|
||
|
||
func (d *acDevice) GetSwing(ctx context.Context) (string, error) {
|
||
if !d.propSwing.valid {
|
||
return ACSwingOff, xiaomi.ErrNotSupported
|
||
}
|
||
prop, err := d.GetProp(ctx, d.propSwing.siid, d.propSwing.piid)
|
||
if err != nil {
|
||
return ACSwingOff, err
|
||
}
|
||
return d.swingMap.name(toInt(prop.Value)), nil
|
||
}
|
||
|
||
func (d *acDevice) SetTargetHumidity(ctx context.Context, pct int) error {
|
||
if !d.propTargetHumidity.valid {
|
||
return xiaomi.ErrNotSupported
|
||
}
|
||
return d.SetProp(ctx, d.propTargetHumidity.siid, d.propTargetHumidity.piid, pct)
|
||
}
|
||
|
||
func (d *acDevice) GetTargetHumidity(ctx context.Context) (int, error) {
|
||
if !d.propTargetHumidity.valid {
|
||
return 0, xiaomi.ErrNotSupported
|
||
}
|
||
prop, err := d.GetProp(ctx, d.propTargetHumidity.siid, d.propTargetHumidity.piid)
|
||
if err != nil {
|
||
return 0, err
|
||
}
|
||
return toInt(prop.Value), nil
|
||
}
|
||
|
||
func (d *acDevice) GetCurrentHumidity(ctx context.Context) (int, error) {
|
||
if !d.propCurrentHumidity.valid {
|
||
return 0, xiaomi.ErrNotSupported
|
||
}
|
||
prop, err := d.GetProp(ctx, d.propCurrentHumidity.siid, d.propCurrentHumidity.piid)
|
||
if err != nil {
|
||
return 0, err
|
||
}
|
||
return toInt(prop.Value), nil
|
||
}
|
||
|
||
func (d *acDevice) SetAuxHeat(ctx context.Context, on bool) error {
|
||
if !d.propAuxHeat.valid {
|
||
return xiaomi.ErrNotSupported
|
||
}
|
||
return d.SetProp(ctx, d.propAuxHeat.siid, d.propAuxHeat.piid, on)
|
||
}
|
||
|
||
func (d *acDevice) GetAuxHeat(ctx context.Context) (bool, error) {
|
||
if !d.propAuxHeat.valid {
|
||
return false, xiaomi.ErrNotSupported
|
||
}
|
||
prop, err := d.GetProp(ctx, d.propAuxHeat.siid, d.propAuxHeat.piid)
|
||
if err != nil {
|
||
return false, err
|
||
}
|
||
if b, ok := prop.Value.(bool); ok {
|
||
return b, nil
|
||
}
|
||
return false, nil
|
||
}
|
||
|
||
func (d *acDevice) SetEco(ctx context.Context, on bool) error {
|
||
if !d.propEco.valid {
|
||
return xiaomi.ErrNotSupported
|
||
}
|
||
return d.SetProp(ctx, d.propEco.siid, d.propEco.piid, on)
|
||
}
|
||
|
||
func (d *acDevice) GetEco(ctx context.Context) (bool, error) {
|
||
if !d.propEco.valid {
|
||
return false, xiaomi.ErrNotSupported
|
||
}
|
||
prop, err := d.GetProp(ctx, d.propEco.siid, d.propEco.piid)
|
||
if err != nil {
|
||
return false, err
|
||
}
|
||
if b, ok := prop.Value.(bool); ok {
|
||
return b, nil
|
||
}
|
||
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(*ACProps, interface{})
|
||
valType byte // 'b'=bool, 'i'=int, 'f'=float, 's'=string
|
||
}
|
||
|
||
func (d *acDevice) OnPropsChanged(handler func(props ACProps, changed string)) (string, error) {
|
||
fields := d.propFields()
|
||
if len(fields) == 0 {
|
||
return "", xiaomi.ErrNotSupported
|
||
}
|
||
// 每个属性精确订阅 siid/piid,避免 lumi.acpartner.mcn02 等设备
|
||
// siid/piid 冲突时把风速 int 路由到 on 槽位
|
||
var firstID string
|
||
for i, f := range fields {
|
||
idx := i
|
||
subID, err := d.SubProp(f.siid, f.piid, func(did string, prop *xiaomi.PropertyValue) {
|
||
// lumi.acpartner.mcn02 固件 bug:on 通道会收到 int 值,静默丢弃
|
||
switch f.valType {
|
||
case 'b':
|
||
if _, ok := prop.Value.(bool); !ok {
|
||
return
|
||
}
|
||
case 's':
|
||
if _, ok := prop.Value.(string); !ok {
|
||
return
|
||
}
|
||
}
|
||
fields[idx].fill(&d.propsCache, prop.Value)
|
||
handler(d.propsCache, f.name)
|
||
})
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
if firstID == "" {
|
||
firstID = subID
|
||
}
|
||
}
|
||
return firstID, nil
|
||
}
|
||
|
||
func (d *acDevice) propFields() []propField {
|
||
r := d.getSpecResolver()
|
||
var fields []propField
|
||
add := func(sp siidPiid, name string, specType string, fill func(*ACProps, interface{})) {
|
||
if sp.valid {
|
||
vt := byte(0)
|
||
switch specType {
|
||
case "bool":
|
||
vt = 'b'
|
||
case "uint8", "uint16", "uint32", "int8", "int16", "int32":
|
||
vt = 'i'
|
||
case "float":
|
||
vt = 'f'
|
||
case "string":
|
||
vt = 's'
|
||
}
|
||
fields = append(fields, propField{sp.siid, sp.piid, name, fill, vt})
|
||
}
|
||
}
|
||
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 *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 *ACProps, v interface{}) {
|
||
s.Swing = d.swingMap.name(toInt(v))
|
||
})
|
||
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
|
||
}
|
||
|
||
func specFormat(r *specResolver, propType string) string {
|
||
if r == nil {
|
||
return ""
|
||
}
|
||
return r.format(propType)
|
||
}
|
||
|
||
// toFloat converts an interface{} to float64.
|
||
func toFloat(v interface{}) float64 {
|
||
switch n := v.(type) {
|
||
case float64:
|
||
return n
|
||
case float32:
|
||
return float64(n)
|
||
case int:
|
||
return float64(n)
|
||
case int64:
|
||
return float64(n)
|
||
}
|
||
return 0
|
||
}
|
||
|
||
// 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) 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 {
|
||
keys[i] = xiaomi.PropKey{DID: d.info.DID, SIID: f.siid, PIID: f.piid}
|
||
}
|
||
props, err := d.client.GetProps(ctx, keys)
|
||
if err != nil {
|
||
return d.propsCache, err
|
||
}
|
||
result := ACProps{}
|
||
for _, p := range props {
|
||
for _, f := range fields {
|
||
if f.siid == p.SIID && f.piid == p.PIID {
|
||
f.fill(&result, p.Value)
|
||
break
|
||
}
|
||
}
|
||
}
|
||
d.propsCache = result
|
||
return result, nil
|
||
}
|