refactor: 重大架构升级 — HomeID/RoomID int64化 + 设备驱动重构 + 结构化日志

主要变更:

**类型系统重构**
- HomeInfo.ID/RoomInfo.ID/HomeID 从 string 改为 int64,json tag 使用 ,string
- GetHome/GetRooms/GetRoomDevices 接口同步修改
- 新增 int64FromStr 工具函数,更新 parseHomeInfo 解析逻辑

**新增 Speaker 设备**
- 新增 KindSpeaker 类型
- classify 映射覆盖 speaker/bluetooth-speaker/alarm-clock/media-player/playback/music

**设备驱动重构**
- AC 驱动彻底重写:从硬编码常量改为基于 SPEC ValueList 的动态 valMapper
- 所有设备(Fan/Humidifier/Light/OccupancySensor/Switch/TempHumiditySensor/Thermostat/Vacuum/WaterHeater/Cover)新增 GetState() 和 FetchState(ctx) 接口
- base.go 新增 specResolver.format() 方法

**MIoT 模块增强**
- MIoTClient 新增 homeIDs 字段,支持按家庭过滤设备
- RefreshDevices 支持 homeIDs 参数
- SpecStdLib 引入 logger.Logger,log.Printf 全部迁移为结构化日志

**其他**
- Config 新增 RedirectURL 和 HomeIDs 字段
- AuthInfo.ExpiresIn 类型从 int 改为 int32
- UUID 生成简化为 hex.EncodeToString
- 新增 github.com/sirupsen/logrus 依赖
- .gitignore 新增 examples/*/data/ 忽略规则
- 删除已提交的 config.json 敏感配置
This commit is contained in:
2026-07-13 10:40:20 +08:00
parent 8ffc9e4e44
commit b808e51b3e
68 changed files with 2506 additions and 688 deletions
+215 -151
View File
@@ -2,89 +2,72 @@ package devices
import (
"context"
"fmt"
"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)
name := strings.ToLower(item.Desc)
vm.val2name[v] = name
vm.name2val[name] = v
vm.names = append(vm.names, name)
}
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[name]
return v, ok
}
func (vm *valMapper) allNames() []string {
if vm == nil { return nil }
return vm.names
}
// ========== 空调模式/风速/摆风常量 ==========
// ACMode 空调模式
type ACMode string
const (
ModeCool ACMode = "cool"
ModeHeat ACMode = "heat"
ModeFan ACMode = "fan"
ModeDry ACMode = "dry"
ModeAuto ACMode = "auto"
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"
)
// modeValueMap 模式值映射(uint8 → ACMode)
var modeValueMap = map[int]ACMode{
0: ModeCool,
1: ModeHeat,
2: ModeFan,
3: ModeDry,
4: ModeAuto,
}
var modeReverseMap = map[ACMode]int{
ModeCool: 0,
ModeHeat: 1,
ModeFan: 2,
ModeDry: 3,
ModeAuto: 4,
}
// FanSpeed 风速
type FanSpeed string
const (
SpeedLow FanSpeed = "low"
SpeedMedium FanSpeed = "medium"
SpeedHigh FanSpeed = "high"
SpeedAuto FanSpeed = "auto"
)
var fanSpeedValueMap = map[int]FanSpeed{
0: SpeedLow,
1: SpeedMedium,
2: SpeedHigh,
3: SpeedAuto,
}
var fanSpeedReverseMap = map[FanSpeed]int{
SpeedLow: 0,
SpeedMedium: 1,
SpeedHigh: 2,
SpeedAuto: 3,
}
// SwingMode 摆风模式
type SwingMode string
const (
SwingOff SwingMode = "off"
SwingVertical SwingMode = "vertical"
SwingHorizontal SwingMode = "horizontal"
SwingBoth SwingMode = "both"
)
var swingValueMap = map[int]SwingMode{
0: SwingOff,
1: SwingVertical,
2: SwingHorizontal,
3: SwingBoth,
}
var swingReverseMap = map[SwingMode]int{
SwingOff: 0,
SwingVertical: 1,
SwingHorizontal: 2,
SwingBoth: 3,
}
// ========== AirConditioner 接口 ==========
// AirConditioner 空调统一控制接口
@@ -102,8 +85,8 @@ type AirConditioner interface {
HasEco() bool
// 模式控制
SetMode(ctx context.Context, mode ACMode) error
GetMode(ctx context.Context) (ACMode, error)
SetMode(ctx context.Context, mode string) error
GetMode(ctx context.Context) (string, error)
// 温度控制
SetTargetTemp(ctx context.Context, celsius float64) error
@@ -111,12 +94,12 @@ type AirConditioner interface {
GetCurrentTemp(ctx context.Context) (float64, error) // 室内温度
// 风速控制
SetFanSpeed(ctx context.Context, speed FanSpeed) error
GetFanSpeed(ctx context.Context) (FanSpeed, error)
SetFanSpeed(ctx context.Context, speed string) error
GetFanSpeed(ctx context.Context) (string, error)
// 摆风控制
SetSwing(ctx context.Context, mode SwingMode) error
GetSwing(ctx context.Context) (SwingMode, error)
SetSwing(ctx context.Context, mode string) error
GetSwing(ctx context.Context) (string, error)
// 湿度
SetTargetHumidity(ctx context.Context, pct int) error
@@ -133,20 +116,22 @@ type AirConditioner interface {
// 订阅
OnStateChanged(handler func(state ACState, changed string)) (string, error)
GetState() ACState
FetchState(ctx context.Context) (ACState, error)
}
// ACState 空调完整状态(字段名对齐 SPEC type)
type ACState struct {
On bool `json:"on"` // SPEC: "on"
Mode ACMode `json:"mode"` // SPEC: "mode"
TargetTemperature float64 `json:"targetTemperature"` // SPEC: "target-temperature"
Temperature float64 `json:"temperature"` // SPEC: "temperature" (当前温度)
TargetHumidity int `json:"targetHumidity"` // SPEC: "target-humidity"
CurrentHumidity int `json:"currentHumidity"` // SPEC: "relative-humidity"
FanLevel FanSpeed `json:"fanLevel"` // SPEC: "fan-level"
Swing SwingMode `json:"swing"` // SPEC: "swing"
AuxHeat bool `json:"auxHeat"` // SPEC: "heater"
Eco bool `json:"eco"` // SPEC: "eco"
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"`
}
type acDevice struct {
@@ -162,6 +147,10 @@ type acDevice struct {
propAuxHeat siidPiid // "aux-heat" or "heater"
propEco siidPiid // "eco"
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 推送维护的本地缓存
}
@@ -171,11 +160,32 @@ func NewAirConditioner(client *xiaomi.Client, info *xiaomi.DeviceInfo) (AirCondi
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
}
// "on" is required
siid, piid, ok := resolver.find("on")
if !ok {
@@ -186,6 +196,7 @@ func NewAirConditioner(client *xiaomi.Client, info *xiaomi.DeviceInfo) (AirCondi
// 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 s, p, ok := resolver.find("target-temperature"); ok {
d.propTargetTemp = siidPiid{s, p, true}
@@ -195,14 +206,18 @@ func NewAirConditioner(client *xiaomi.Client, info *xiaomi.DeviceInfo) (AirCondi
}
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}
@@ -256,30 +271,26 @@ 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) SetMode(ctx context.Context, mode ACMode) error {
func (d *acDevice) SetMode(ctx context.Context, mode string) error {
if !d.propMode.valid {
return xiaomi.ErrNotSupported
}
val, ok := modeReverseMap[mode]
v, ok := d.modeMap.val(mode)
if !ok {
val = 4 // default auto
return xiaomi.ErrNotSupported
}
return d.SetProp(ctx, d.propMode.siid, d.propMode.piid, val)
return d.SetProp(ctx, d.propMode.siid, d.propMode.piid, v)
}
func (d *acDevice) GetMode(ctx context.Context) (ACMode, error) {
func (d *acDevice) GetMode(ctx context.Context) (string, error) {
if !d.propMode.valid {
return ModeAuto, xiaomi.ErrNotSupported
return ACModeAuto, xiaomi.ErrNotSupported
}
prop, err := d.GetProp(ctx, d.propMode.siid, d.propMode.piid)
if err != nil {
return ModeAuto, err
return ACModeAuto, err
}
v := toInt(prop.Value)
if m, ok := modeValueMap[v]; ok {
return m, nil
}
return ModeAuto, nil
return d.modeMap.name(toInt(prop.Value)), nil
}
func (d *acDevice) SetTargetTemp(ctx context.Context, celsius float64) error {
@@ -311,56 +322,48 @@ func (d *acDevice) GetCurrentTemp(ctx context.Context) (float64, error) {
return toFloat(prop.Value), nil
}
func (d *acDevice) SetFanSpeed(ctx context.Context, speed FanSpeed) error {
func (d *acDevice) SetFanSpeed(ctx context.Context, speed string) error {
if !d.propFanSpeed.valid {
return xiaomi.ErrNotSupported
}
val, ok := fanSpeedReverseMap[speed]
v, ok := d.fanMap.val(speed)
if !ok {
val = 3 // default auto
return xiaomi.ErrNotSupported
}
return d.SetProp(ctx, d.propFanSpeed.siid, d.propFanSpeed.piid, val)
return d.SetProp(ctx, d.propFanSpeed.siid, d.propFanSpeed.piid, v)
}
func (d *acDevice) GetFanSpeed(ctx context.Context) (FanSpeed, error) {
func (d *acDevice) GetFanSpeed(ctx context.Context) (string, error) {
if !d.propFanSpeed.valid {
return SpeedAuto, xiaomi.ErrNotSupported
return ACFanAuto, xiaomi.ErrNotSupported
}
prop, err := d.GetProp(ctx, d.propFanSpeed.siid, d.propFanSpeed.piid)
if err != nil {
return SpeedAuto, err
return ACFanAuto, err
}
v := toInt(prop.Value)
if s, ok := fanSpeedValueMap[v]; ok {
return s, nil
}
return SpeedAuto, nil
return d.fanMap.name(toInt(prop.Value)), nil
}
func (d *acDevice) SetSwing(ctx context.Context, mode SwingMode) error {
func (d *acDevice) SetSwing(ctx context.Context, mode string) error {
if !d.propSwing.valid {
return xiaomi.ErrNotSupported
}
val, ok := swingReverseMap[mode]
v, ok := d.swingMap.val(mode)
if !ok {
val = 0
return xiaomi.ErrNotSupported
}
return d.SetProp(ctx, d.propSwing.siid, d.propSwing.piid, val)
return d.SetProp(ctx, d.propSwing.siid, d.propSwing.piid, v)
}
func (d *acDevice) GetSwing(ctx context.Context) (SwingMode, error) {
func (d *acDevice) GetSwing(ctx context.Context) (string, error) {
if !d.propSwing.valid {
return SwingOff, xiaomi.ErrNotSupported
return ACSwingOff, xiaomi.ErrNotSupported
}
prop, err := d.GetProp(ctx, d.propSwing.siid, d.propSwing.piid)
if err != nil {
return SwingOff, err
return ACSwingOff, err
}
v := toInt(prop.Value)
if s, ok := swingValueMap[v]; ok {
return s, nil
}
return SwingOff, nil
return d.swingMap.name(toInt(prop.Value)), nil
}
func (d *acDevice) SetTargetHumidity(ctx context.Context, pct int) error {
@@ -439,6 +442,7 @@ type propField struct {
siid, piid int
name string
fill func(*ACState, interface{})
valType byte // 'b'=bool, 'i'=int, 'f'=float, 's'=string
}
func (d *acDevice) OnStateChanged(handler func(state ACState, changed string)) (string, error) {
@@ -446,46 +450,81 @@ func (d *acDevice) OnStateChanged(handler func(state ACState, changed string)) (
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)
return
// 每个属性精确订阅 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.stateCache, prop.Value)
handler(d.stateCache, 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, fill func(*ACState, interface{})) {
add := func(sp siidPiid, name string, specType string, fill func(*ACState, interface{})) {
if sp.valid {
fields = append(fields, propField{sp.siid, sp.piid, name, fill})
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", func(s *ACState, v interface{}) { s.On, _ = v.(bool) })
add(d.propMode, "mode", func(s *ACState, v interface{}) {
switch m := v.(type) {
case string: s.Mode = ACMode(m)
case float64: s.Mode = ACMode(fmt.Sprintf("%.0f", m))
}
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{}) {
s.Mode = d.modeMap.name(toInt(v))
})
add(d.propTargetTemp, "target-temperature", func(s *ACState, v interface{}) { s.TargetTemperature = toFloat(v) })
add(d.propCurrentTemp, "temperature", func(s *ACState, v interface{}) { s.Temperature = toFloat(v) })
add(d.propFanSpeed, "fan-level", func(s *ACState, v interface{}) {
if v, ok := v.(float64); ok { s.FanLevel = FanSpeed(fmt.Sprintf("%.0f", 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{}) {
s.FanLevel = d.fanMap.name(toInt(v))
})
add(d.propSwing, "swing", func(s *ACState, v interface{}) {
if v, ok := v.(float64); ok { s.Swing = SwingMode(fmt.Sprintf("%.0f", v)) }
add(d.propSwing, "swing", specFormat(r, "swing-mode"), func(s *ACState, v interface{}) {
s.Swing = d.swingMap.name(toInt(v))
})
add(d.propTargetHumidity, "target-humidity", func(s *ACState, v interface{}) { s.TargetHumidity = int(toFloat(v)) })
add(d.propCurrentHumidity, "relative-humidity", func(s *ACState, v interface{}) { s.CurrentHumidity = int(toFloat(v)) })
add(d.propAuxHeat, "heater", func(s *ACState, v interface{}) { s.AuxHeat, _ = v.(bool) })
add(d.propEco, "eco", func(s *ACState, v interface{}) { s.Eco, _ = v.(bool) })
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) })
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) {
@@ -500,3 +539,28 @@ func toFloat(v interface{}) float64 {
}
return 0
}
func (d *acDevice) GetState() ACState { return d.stateCache }
func (d *acDevice) FetchState(ctx context.Context) (ACState, 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.stateCache, err
}
state := ACState{}
for _, p := range props {
for _, f := range fields {
if f.siid == p.SIID && f.piid == p.PIID {
f.fill(&state, p.Value)
break
}
}
}
d.stateCache = state
return state, nil
}
+11
View File
@@ -167,3 +167,14 @@ func (r *specResolver) find(propType string) (siid, piid int, found bool) {
}
return 0, 0, false
}
// format returns the SPEC Format for a property type ("bool", "uint8", "float", "string").
func (r *specResolver) format(propType string) string {
if r == nil {
return ""
}
if prop, ok := r.byType[propType]; ok {
return prop.Format
}
return ""
}
+10
View File
@@ -16,6 +16,8 @@ type Cover interface {
GetPosition(ctx context.Context) (int, error)
OnStateChanged(handler func(position int)) (string, error)
GetState() int
FetchState(ctx context.Context) (int, error)
}
type coverDevice struct {
@@ -81,3 +83,11 @@ func (d *coverDevice) OnStateChanged(handler func(position int)) (string, error)
}
})
}
func (d *coverDevice) GetState() int { return d.stateCache }
func (d *coverDevice) FetchState(ctx context.Context) (int, error) {
pos, err := d.GetPosition(ctx)
if err != nil { return d.stateCache, err }
d.stateCache = pos
return pos, nil
}
+6 -20
View File
@@ -53,34 +53,20 @@ func TestToFloat(t *testing.T) {
}
func TestAirConditionerModeConstants(t *testing.T) {
// Verify all mode values have reverse mappings
for _, m := range []ACMode{ModeCool, ModeHeat, ModeFan, ModeDry, ModeAuto} {
if _, ok := modeReverseMap[m]; !ok {
t.Errorf("missing reverse mapping for %s", m)
}
}
// Verify all value entries map back correctly
for v, m := range modeValueMap {
if r, ok := modeReverseMap[m]; !ok || r != v {
t.Errorf("modeReverseMap[modeValueMap[%d]] = %v, want %d", v, r, v)
}
if ACModeCool != "cool" || ACModeHeat != "heat" || ACModeDry != "dry" || ACModeAuto != "auto" {
t.Error("AC mode constants mismatch")
}
}
func TestFanSpeedConstants(t *testing.T) {
for _, s := range []FanSpeed{SpeedLow, SpeedMedium, SpeedHigh, SpeedAuto} {
if _, ok := fanSpeedReverseMap[s]; !ok {
t.Errorf("missing reverse mapping for %s", s)
}
if ACFanLow != "low" || ACFanAuto != "auto" {
t.Error("Fan speed constants mismatch")
}
}
func TestSwingModeConstants(t *testing.T) {
for _, s := range []SwingMode{SwingOff, SwingVertical, SwingHorizontal, SwingBoth} {
if _, ok := swingReverseMap[s]; !ok {
t.Errorf("missing reverse mapping for %s", s)
}
if ACSwingOff != "off" || ACSwingVertical != "vertical" {
t.Error("Swing mode constants mismatch")
}
}
+2
View File
@@ -87,6 +87,8 @@ func createImpl(client *xiaomi.Client, info *xiaomi.DeviceInfo) (interface{}, er
ctrl, err = NewOccupancySensor(client, info)
case matchURN(urn, "sensor-temp", "sensor-humidity", "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)
default:
// Not a supported device type — not an error, just returns nil
return nil, nil
+24
View File
@@ -22,6 +22,8 @@ type Fan interface {
IsNaturalWind(ctx context.Context) (bool, error)
OnStateChanged(handler func(state FanState, changed string)) (string, error)
GetState() FanState
FetchState(ctx context.Context) (FanState, error)
}
// FanState 风扇完整状态(MQTT 缓存)
@@ -193,3 +195,25 @@ func (d *fanDevice) OnStateChanged(handler func(state FanState, changed string))
}
})
}
func (d *fanDevice) GetState() FanState { return d.stateCache }
func (d *fanDevice) FetchState(ctx context.Context) (FanState, 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}) }
props, err := d.client.GetProps(ctx, keys)
if err != nil { return d.stateCache, err }
state := FanState{}
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
}
}
d.stateCache = state
return state, nil
}
+22
View File
@@ -17,6 +17,8 @@ type Humidifier interface {
GetFanLevel(ctx context.Context) (int, error)
OnStateChanged(handler func(state HumidifierState, changed string)) (string, error)
GetState() HumidifierState
FetchState(ctx context.Context) (HumidifierState, error)
}
// HumidifierState 加湿器状态(MQTT 缓存)
@@ -105,3 +107,23 @@ func (d *humidifierDevice) OnStateChanged(handler func(state HumidifierState, ch
}
})
}
func (d *humidifierDevice) GetState() HumidifierState { return d.stateCache }
func (d *humidifierDevice) FetchState(ctx context.Context) (HumidifierState, 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{}
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)
}
}
d.stateCache = state
return state, nil
}
+25
View File
@@ -30,6 +30,8 @@ type Light interface {
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)
}
// LightState 灯光完整状态(字段名对齐 SPEC type)
@@ -241,3 +243,26 @@ func toInt(v interface{}) int {
}
return 0
}
func (d *lightDevice) GetState() LightState { return d.stateCache }
func (d *lightDevice) FetchState(ctx context.Context) (LightState, 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{}
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.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} }
}
}
d.stateCache = state
return state, nil
}
+31
View File
@@ -14,6 +14,8 @@ type OccupancySensor interface {
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)
}
type occupancyDevice struct {
@@ -71,3 +73,32 @@ func (d *occupancyDevice) OnStateChanged(handler func(occupied bool, illuminance
handler(occ, lux)
})
}
func (d *occupancyDevice) GetState() (bool, float64) {
b, _ := d.stateCache[0].(bool)
f, _ := d.stateCache[1].(float64)
return b, f
}
func (d *occupancyDevice) FetchState(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}) }
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)
return b, f, err
}
var occupied bool
var illuminance float64
for _, p := 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)
}
}
d.stateCache = [2]interface{}{occupied, illuminance}
return occupied, illuminance, nil
}
+45
View File
@@ -0,0 +1,45 @@
package devices
// ── 设备属性名常量 ──
// 用于 OnStateChanged 回调的 changed 字段,switch 前先用 deviceType 区分设备类型。
const (
// ── 通用 ──
PropOn = "on" // 所有设备
PropMode = "mode" // AC / Fan / Vacuum
PropSwing = "swing" // AC / Fan
PropFanLevel = "fan-level" // AC / Fan / Humidifier / Vacuum
PropTargetTemperature = "target-temperature" // AC / Thermostat / WaterHeater
PropTemperature = "temperature" // AC / Thermostat / WaterHeater
PropTargetHumidity = "target-humidity" // AC / Humidifier
PropRelativeHumidity = "relative-humidity" // AC
// ── 空调专属 ──
PropHeater = "heater"
PropEco = "eco"
// ── 灯光 ──
PropBrightness = "brightness"
PropColorTemperature = "color-temperature"
PropColor = "color"
// ── 扫地机 ──
PropBattery = "battery"
// ── 窗帘 ──
PropPosition = "position"
// ── 传感器 ──
PropTemperatureHumidity = "temperature_humidity"
PropOccupancy = "occupancy"
// ── 音箱 ──
PropVolume = "volume"
PropMute = "mute"
PropPlayState = "play-state"
)
const (
PropValueTrue = "true"
PropValueFalse = "false"
)
+434
View File
@@ -0,0 +1,434 @@
package devices
import (
"context"
"strings"
"xiaomihome/miot"
"xiaomihome/xiaomi"
)
// Speaker 音箱通用控制接口
type Speaker interface {
Device
TurnOn(ctx context.Context) error
TurnOff(ctx context.Context) error
Toggle(ctx context.Context) error
// 能力查询
HasVolume() bool
HasMute() bool
HasPlayControl() bool
// 音量 (0-100)
SetVolume(ctx context.Context, pct int) error
GetVolume(ctx context.Context) (int, error)
// 静音
SetMute(ctx context.Context, mute bool) error
GetMute(ctx context.Context) (bool, error)
// 播放控制 (0=停止, 1=播放, 2=暂停)
SetPlayState(ctx context.Context, state int) error
GetPlayState(ctx context.Context) (int, error)
// ── 智能音箱 (intelligent-speaker) ──
// 睡眠模式
HasSleepMode() bool
SetSleepMode(ctx context.Context, sleep bool) error
GetSleepMode(ctx context.Context) (bool, error)
// 音频 ID (只读/通知)
GetAudioID(ctx context.Context) (string, error)
// 动作
WakeUp(ctx context.Context) error
PlayRadio(ctx context.Context) error
PlayText(ctx context.Context, text string) error
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)
}
// SpeakerState 音箱完整状态(字段名对齐 SPEC type)
type SpeakerState struct {
On bool `json:"on"` // SPEC: "on"
Volume int `json:"volume"` // SPEC: "volume"
Mute bool `json:"mute"` // SPEC: "mute"
PlayState int `json:"playState"` // SPEC: "tds-in" or "playing-state" (0=stop,1=play,2=pause)
SleepMode bool `json:"sleepMode"` // SPEC: "sleep-mode" (intelligent-speaker)
AudioID string `json:"audioId"` // SPEC: "audio-id" (intelligent-speaker, read-only)
}
// isAction 智能音箱动作 siid/aiid 对
type isAction struct{ siid, aiid int }
type speakerDevice struct {
BaseDevice
propOn siidPiid // "on"
propVolume siidPiid // "volume"
propMute siidPiid // "mute"
propPlayState siidPiid // "playing-state" or "tds-in"
propSleepMode siidPiid // "sleep-mode" (intelligent-speaker)
propAudioID siidPiid // "audio-id" (intelligent-speaker)
// intelligent-speaker 服务 siid
isSiid int
// 动作
actWakeUp isAction
actPlayRadio isAction
actPlayText isAction
actExecuteTextDirective isAction
actPlayMusic isAction
stateCache SpeakerState
}
// NewSpeaker creates a Speaker control for the given device.
func NewSpeaker(client *xiaomi.Client, info *xiaomi.DeviceInfo) (Speaker, error) {
d := &speakerDevice{
BaseDevice: NewBaseDevice(client, info),
}
dev := d.getMIoTDevice()
if dev == nil {
return nil, xiaomi.ErrNoSPEC
}
spec := dev.SpecInstance()
if spec == nil {
return nil, xiaomi.ErrNoSPEC
}
// 定位 intelligent-speaker 服务
var isSvc *miot.MIoTSpecService
for _, svc := range spec.Services {
if shortName(svc.Type) == "intelligent-speaker" {
isSvc = svc
d.isSiid = svc.IID
break
}
}
// 先用 resolver 解析通用属性
resolver := d.getSpecResolver()
if resolver == nil {
return nil, xiaomi.ErrNoSPEC
}
siid, piid, ok := resolver.find("on")
if !ok {
return nil, xiaomi.ErrNotSupported
}
d.propOn = siidPiid{siid, piid, true}
if s, p, ok := resolver.find("volume"); ok {
d.propVolume = siidPiid{s, p, true}
}
if s, p, ok := resolver.find("mute"); ok {
d.propMute = siidPiid{s, p, true}
}
if s, p, ok := resolver.find("playing-state"); ok {
d.propPlayState = siidPiid{s, p, true}
} else if s, p, ok := resolver.find("tds-in"); ok {
d.propPlayState = siidPiid{s, p, true}
}
// intelligent-speaker 属性
if isSvc != nil {
for _, p := range isSvc.Properties {
switch shortName(p.Type) {
case "sleep-mode":
d.propSleepMode = siidPiid{d.isSiid, p.PIID, true}
case "audio-id":
d.propAudioID = siidPiid{d.isSiid, p.PIID, true}
}
}
// 动作
for _, a := range isSvc.Actions {
switch shortName(a.Type) {
case "wake-up":
d.actWakeUp = isAction{d.isSiid, a.AIID}
case "play-radio":
d.actPlayRadio = isAction{d.isSiid, a.AIID}
case "play-text":
d.actPlayText = isAction{d.isSiid, a.AIID}
case "execute-text-directive":
d.actExecuteTextDirective = isAction{d.isSiid, a.AIID}
case "play-music":
d.actPlayMusic = isAction{d.isSiid, a.AIID}
}
}
}
return d, nil
}
func (d *speakerDevice) 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 *speakerDevice) 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 *speakerDevice) Toggle(ctx context.Context) error {
s := d.GetState()
if s.On {
return d.TurnOff(ctx)
}
return d.TurnOn(ctx)
}
func (d *speakerDevice) HasVolume() bool { return d.propVolume.valid }
func (d *speakerDevice) HasMute() bool { return d.propMute.valid }
func (d *speakerDevice) HasPlayControl() bool { return d.propPlayState.valid }
func (d *speakerDevice) SetVolume(ctx context.Context, pct int) error {
if !d.propVolume.valid {
return xiaomi.ErrNotSupported
}
return d.SetProp(ctx, d.propVolume.siid, d.propVolume.piid, pct)
}
func (d *speakerDevice) GetVolume(ctx context.Context) (int, error) {
if !d.propVolume.valid {
return 0, xiaomi.ErrNotSupported
}
prop, err := d.GetProp(ctx, d.propVolume.siid, d.propVolume.piid)
if err != nil {
return 0, err
}
return toInt(prop.Value), nil
}
func (d *speakerDevice) SetMute(ctx context.Context, mute bool) error {
if !d.propMute.valid {
return xiaomi.ErrNotSupported
}
return d.SetProp(ctx, d.propMute.siid, d.propMute.piid, mute)
}
func (d *speakerDevice) GetMute(ctx context.Context) (bool, error) {
if !d.propMute.valid {
return false, xiaomi.ErrNotSupported
}
prop, err := d.GetProp(ctx, d.propMute.siid, d.propMute.piid)
if err != nil {
return false, err
}
if b, ok := prop.Value.(bool); ok {
return b, nil
}
return false, nil
}
func (d *speakerDevice) SetPlayState(ctx context.Context, state int) error {
if !d.propPlayState.valid {
return xiaomi.ErrNotSupported
}
return d.SetProp(ctx, d.propPlayState.siid, d.propPlayState.piid, state)
}
func (d *speakerDevice) GetPlayState(ctx context.Context) (int, error) {
if !d.propPlayState.valid {
return 0, xiaomi.ErrNotSupported
}
prop, err := d.GetProp(ctx, d.propPlayState.siid, d.propPlayState.piid)
if err != nil {
return 0, err
}
return toInt(prop.Value), nil
}
// ── 智能音箱属性 ──
func (d *speakerDevice) HasSleepMode() bool { return d.propSleepMode.valid }
func (d *speakerDevice) SetSleepMode(ctx context.Context, sleep bool) error {
if !d.propSleepMode.valid {
return xiaomi.ErrNotSupported
}
return d.SetProp(ctx, d.propSleepMode.siid, d.propSleepMode.piid, sleep)
}
func (d *speakerDevice) GetSleepMode(ctx context.Context) (bool, error) {
if !d.propSleepMode.valid {
return false, xiaomi.ErrNotSupported
}
prop, err := d.GetProp(ctx, d.propSleepMode.siid, d.propSleepMode.piid)
if err != nil {
return false, err
}
if b, ok := prop.Value.(bool); ok {
return b, nil
}
return false, nil
}
func (d *speakerDevice) GetAudioID(ctx context.Context) (string, error) {
if !d.propAudioID.valid {
return "", xiaomi.ErrNotSupported
}
prop, err := d.GetProp(ctx, d.propAudioID.siid, d.propAudioID.piid)
if err != nil {
return "", err
}
if s, ok := prop.Value.(string); ok {
return s, nil
}
return "", nil
}
// ── 智能音箱动作 ──
func (d *speakerDevice) WakeUp(ctx context.Context) error {
if d.actWakeUp.siid == 0 {
return xiaomi.ErrNotSupported
}
_, err := d.Action(ctx, d.actWakeUp.siid, d.actWakeUp.aiid, nil)
return err
}
func (d *speakerDevice) PlayRadio(ctx context.Context) error {
if d.actPlayRadio.siid == 0 {
return xiaomi.ErrNotSupported
}
_, err := d.Action(ctx, d.actPlayRadio.siid, d.actPlayRadio.aiid, nil)
return err
}
func (d *speakerDevice) PlayText(ctx context.Context, text string) error {
if d.actPlayText.siid == 0 {
return xiaomi.ErrNotSupported
}
_, err := d.Action(ctx, d.actPlayText.siid, d.actPlayText.aiid, []interface{}{
map[string]interface{}{"piid": 1, "value": text},
})
return err
}
func (d *speakerDevice) ExecuteTextDirective(ctx context.Context, text string, silent bool) error {
if d.actExecuteTextDirective.siid == 0 {
return xiaomi.ErrNotSupported
}
_, err := d.Action(ctx, d.actExecuteTextDirective.siid, d.actExecuteTextDirective.aiid, []interface{}{
map[string]interface{}{"piid": 1, "value": text},
map[string]interface{}{"piid": 2, "value": silent},
})
return err
}
func (d *speakerDevice) PlayMusic(ctx context.Context) error {
if d.actPlayMusic.siid == 0 {
return xiaomi.ErrNotSupported
}
_, err := d.Action(ctx, d.actPlayMusic.siid, d.actPlayMusic.aiid, nil)
return err
}
// ── 订阅 / 状态 ──
func (d *speakerDevice) OnStateChanged(handler func(state SpeakerState, changed string)) (string, error) {
type field struct {
siid, piid int
name string
fill func(*SpeakerState, interface{})
}
var fields []field
add := func(sp siidPiid, name string, fill func(*SpeakerState, 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{}) {
if str, ok := v.(string); ok {
s.AudioID = str
}
})
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)
return
}
}
})
}
func (d *speakerDevice) GetState() SpeakerState { return d.stateCache }
func (d *speakerDevice) FetchState(ctx context.Context) (SpeakerState, error) {
keys := []xiaomi.PropKey{}
add := func(sp siidPiid) {
if sp.valid {
keys = append(keys, xiaomi.PropKey{DID: d.info.DID, SIID: sp.siid, PIID: sp.piid})
}
}
add(d.propOn)
add(d.propVolume)
add(d.propMute)
add(d.propPlayState)
add(d.propSleepMode)
add(d.propAudioID)
props, err := d.client.GetProps(ctx, keys)
if err != nil {
return d.stateCache, err
}
state := SpeakerState{}
for _, p := range props {
switch {
case match(d.propOn, p):
state.On, _ = p.Value.(bool)
case match(d.propVolume, p):
state.Volume = toInt(p.Value)
case match(d.propMute, p):
state.Mute, _ = p.Value.(bool)
case match(d.propPlayState, p):
state.PlayState = toInt(p.Value)
case match(d.propSleepMode, p):
state.SleepMode, _ = p.Value.(bool)
case match(d.propAudioID, p):
if s, ok := p.Value.(string); ok {
state.AudioID = s
}
}
}
d.stateCache = state
return state, nil
}
func match(sp siidPiid, p *xiaomi.PropertyValue) bool {
return sp.valid && p.SIID == sp.siid && p.PIID == sp.piid
}
// shortName extracts the short name from a SPEC URN.
// E.g., "urn:miot-spec-v2:action:wake-up:..." → "wake-up".
func shortName(urn string) string {
parts := strings.Split(urn, ":")
if len(parts) >= 4 {
return parts[3]
}
return urn
}
+23
View File
@@ -24,6 +24,8 @@ type Switch interface {
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)
}
// switchDevice is the concrete Switch implementation.
@@ -161,3 +163,24 @@ func (d *switchDevice) channel(ch int) (siidPiid, bool) {
}
return d.channels[i], true
}
func (d *switchDevice) GetState() []bool { return d.stateCache }
func (d *switchDevice) FetchState(ctx context.Context) ([]bool, 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 }
states := make([]bool, len(d.channels))
for i, c := range d.channels {
for _, p := range props {
if p.SIID == c.siid && p.PIID == c.piid {
states[i], _ = p.Value.(bool)
break
}
}
}
d.stateCache = states
return states, nil
}
+20
View File
@@ -14,6 +14,8 @@ type TempHumiditySensor interface {
GetHumidity(ctx context.Context) (float64, error)
OnStateChanged(handler func(temp, humidity float64)) (string, error)
GetState() (float64, float64)
FetchState(ctx context.Context) (float64, float64, error)
}
type tempHumidityDevice struct {
@@ -71,3 +73,21 @@ func (d *tempHumidityDevice) OnStateChanged(handler func(temp, humidity float64)
handler(d.stateCache[0], d.stateCache[1])
})
}
func (d *tempHumidityDevice) GetState() (float64, float64) { return d.stateCache[0], d.stateCache[1] }
func (d *tempHumidityDevice) FetchState(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]}
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
return state[0], state[1], nil
}
+22
View File
@@ -18,6 +18,8 @@ type Thermostat interface {
IsHeating(ctx context.Context) (bool, error)
OnStateChanged(handler func(state HeaterState, changed string)) (string, error)
GetState() HeaterState
FetchState(ctx context.Context) (HeaterState, error)
}
type thermostatDevice struct {
@@ -108,3 +110,23 @@ func (d *thermostatDevice) OnStateChanged(handler func(state HeaterState, change
}
})
}
func (d *thermostatDevice) GetState() HeaterState { return d.stateCache }
func (d *thermostatDevice) FetchState(ctx context.Context) (HeaterState, 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{}
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)
}
}
d.stateCache = state
return state, nil
}
+10
View File
@@ -16,6 +16,8 @@ type Vacuum interface {
GetStatus(ctx context.Context) (VacuumStatus, error)
OnStateChanged(handler func(state VacuumStatus, changed string)) (string, error)
GetState() VacuumStatus
FetchState(ctx context.Context) (VacuumStatus, error)
}
// VacuumStatus 扫地机运行状态(字段名对齐 SPEC type)
@@ -121,3 +123,11 @@ func (d *vacuumDevice) OnStateChanged(handler func(state VacuumStatus, changed s
}
})
}
func (d *vacuumDevice) GetState() VacuumStatus { return d.stateCache }
func (d *vacuumDevice) FetchState(ctx context.Context) (VacuumStatus, error) {
status, err := d.GetStatus(ctx)
if err != nil { return d.stateCache, err }
d.stateCache = status
return status, nil
}
+22
View File
@@ -16,6 +16,8 @@ type WaterHeater interface {
GetCurrentTemp(ctx context.Context) (float64, error)
OnStateChanged(handler func(state HeaterState, changed string)) (string, error)
GetState() HeaterState
FetchState(ctx context.Context) (HeaterState, error)
}
// HeaterState 热水器状态(MQTT 缓存)
@@ -100,3 +102,23 @@ func (d *waterHeaterDevice) OnStateChanged(handler func(state HeaterState, chang
}
})
}
func (d *waterHeaterDevice) GetState() HeaterState { return d.stateCache }
func (d *waterHeaterDevice) FetchState(ctx context.Context) (HeaterState, 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{}
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)
}
}
d.stateCache = state
return state, nil
}