refactor(logger): 日志改用 logrus 全局包级调用,移除 Logger 注入接口

为什么:并入宿主项目后须与宿主日志体系一致——宿主统一用 pkg/logger 初始化
logrus 全局实例并直接 logrus.Xxx 包级调用;包一层 Logger 接口会让 logrus 的
caller 定位(报告调用文件:行号)失准。

改动:
- 删除 logger 包(Logger 接口 / Default / SetDefault,默认本就是 logrus.StandardLogger)
- 11 个结构体移除 lgr logger.Logger 字段、SetLogger 方法及构造中的 lgr 初始化
- 192 处 lgr.Xxxf 调用改为 logrus.Xxxf,日志消息文本保持不变
- xiaomi.Client 移除 Logger() 访问器,air_conditioner 回读日志去掉判空调用直连 logrus
- miot_i18n.go 两处标准库 log.Printf 改为 logrus.Errorf,统一日志出口
- go.mod:logrus 从 indirect 提升为直接依赖
- goimports/goformat 全树规范化:此前部分文件未 gofmt(单行 if、对齐),本次顺带
  格式化,纯空白/换行变化,无逻辑改动

验证:go build / go vet / go test ./... 全部通过(bridge、miot、xiaomi、devices、specs)
This commit is contained in:
2026-09-15 20:16:26 +08:00
parent 7ae32bd012
commit 2b7de69403
46 changed files with 1166 additions and 903 deletions
+59 -59
View File
@@ -8,66 +8,66 @@ import (
// deviceTypeToKind maps SPEC URN device types to Kind.
// URN format: urn:miot-spec-v2:device:{TYPE}:...
var deviceTypeToKind = map[string]Kind{
"air-conditioner": KindAirConditioner,
"air-condition-outlet": KindAirConditioner,
"air-condition-eco": KindAirConditioner,
"airer": KindAirConditioner,
"ac-partner": KindAirConditioner,
"light": KindLight,
"lightbulb": KindLight,
"night-light": KindLight,
"ceiling-light": KindLight,
"switch": KindSwitch,
"powerstrip": KindSwitch,
"toggle-switch": KindSwitch,
"wireless-switch": KindSwitch,
"curtain-switch": KindSwitch,
"fan": KindFan,
"ceiling-fan": KindFan,
"ventilator": KindFan,
"air-fresh": KindFan,
"air-purifier": KindFan,
"curtain": KindCover,
"airer-rack": KindCover,
"humidifier": KindHumidifier,
"dehumidifier": KindHumidifier,
"air-conditioner": KindAirConditioner,
"air-condition-outlet": KindAirConditioner,
"air-condition-eco": KindAirConditioner,
"airer": KindAirConditioner,
"ac-partner": KindAirConditioner,
"light": KindLight,
"lightbulb": KindLight,
"night-light": KindLight,
"ceiling-light": KindLight,
"switch": KindSwitch,
"powerstrip": KindSwitch,
"toggle-switch": KindSwitch,
"wireless-switch": KindSwitch,
"curtain-switch": KindSwitch,
"fan": KindFan,
"ceiling-fan": KindFan,
"ventilator": KindFan,
"air-fresh": KindFan,
"air-purifier": KindFan,
"curtain": KindCover,
"airer-rack": KindCover,
"humidifier": KindHumidifier,
"dehumidifier": KindHumidifier,
"humidifier-dehumidifier": KindHumidifier,
"vacuum": KindVacuum,
"sweeper": KindVacuum,
"robot-cleaner": KindVacuum,
"water-heater": KindWaterHeater,
"water-cooler": KindWaterHeater,
"kettle": KindWaterHeater,
"heater": KindAirConditioner,
"electric-blanket": KindAirConditioner,
"thermostat": KindAirConditioner,
"speaker": KindSpeaker,
"bluetooth-speaker": KindSpeaker,
"alarm-clock": KindSpeaker,
"media-player": KindSpeaker,
"playback": KindSpeaker,
"music": KindSpeaker,
"gateway": KindGateway,
"plug": KindPlug,
"outlet": KindPlug,
"sensor-occupy": KindOccupancySensor,
"sensor_occupy": KindOccupancySensor,
"occupancy-sensor": KindOccupancySensor,
"occupancy_sensor": KindOccupancySensor,
"motion-sensor": KindOccupancySensor,
"motion_sensor": KindOccupancySensor,
"magnet": KindOccupancySensor,
"sensor": KindSensor,
"sensor-ht": KindSensor,
"sensor_ht": KindSensor,
"sensor-temp": KindSensor,
"sensor_temp": KindSensor,
"sensor-humidity": KindSensor,
"sensor_humidity": KindSensor,
"illumination": KindSensor,
"sensor-smoke": KindSensor,
"sensor_gasm": KindSensor,
"sensor-gasm": KindSensor,
"vacuum": KindVacuum,
"sweeper": KindVacuum,
"robot-cleaner": KindVacuum,
"water-heater": KindWaterHeater,
"water-cooler": KindWaterHeater,
"kettle": KindWaterHeater,
"heater": KindAirConditioner,
"electric-blanket": KindAirConditioner,
"thermostat": KindAirConditioner,
"speaker": KindSpeaker,
"bluetooth-speaker": KindSpeaker,
"alarm-clock": KindSpeaker,
"media-player": KindSpeaker,
"playback": KindSpeaker,
"music": KindSpeaker,
"gateway": KindGateway,
"plug": KindPlug,
"outlet": KindPlug,
"sensor-occupy": KindOccupancySensor,
"sensor_occupy": KindOccupancySensor,
"occupancy-sensor": KindOccupancySensor,
"occupancy_sensor": KindOccupancySensor,
"motion-sensor": KindOccupancySensor,
"motion_sensor": KindOccupancySensor,
"magnet": KindOccupancySensor,
"sensor": KindSensor,
"sensor-ht": KindSensor,
"sensor_ht": KindSensor,
"sensor-temp": KindSensor,
"sensor_temp": KindSensor,
"sensor-humidity": KindSensor,
"sensor_humidity": KindSensor,
"illumination": KindSensor,
"sensor-smoke": KindSensor,
"sensor_gasm": KindSensor,
"sensor-gasm": KindSensor,
}
// classifyKeys 按长度降序排列的 key,保证长/具体 key 先匹配(如 sensor-occupy 先于 sensor)。
+12 -19
View File
@@ -6,8 +6,9 @@ import (
"sync"
"time"
"xiaomihome/logger"
"xiaomihome/miot"
"github.com/sirupsen/logrus"
)
// Client 高级封装客户端(包装 miot.MIoTClient + miot.MIoTHttpClient)
@@ -18,7 +19,6 @@ type Client struct {
mipsCloud *miot.MipsCloudClient
specParser *miot.MIoTSpecParser
uuid string
lgr logger.Logger
deviceMu sync.RWMutex
devices map[string]*miot.MIoTDevice
@@ -95,16 +95,15 @@ func NewClient(ctx context.Context, cfg Config) (*Client, error) {
}
c := &Client{
cfg: cfg,
inner: miotClient,
http: httpClient,
mipsCloud: mipsCloud,
uuid: uuid,
lgr: logger.Default(),
specParser: miot.NewMIoTSpecParser("en", miot.NewMIoTStorage(cfg.DataDir), cfg.DataDir+"/specs"),
devices: make(map[string]*miot.MIoTDevice),
propSubs: make(map[string]string),
eventSubs: make(map[string]string),
cfg: cfg,
inner: miotClient,
http: httpClient,
mipsCloud: mipsCloud,
uuid: uuid,
specParser: miot.NewMIoTSpecParser("en", miot.NewMIoTStorage(cfg.DataDir), cfg.DataDir+"/specs"),
devices: make(map[string]*miot.MIoTDevice),
propSubs: make(map[string]string),
eventSubs: make(map[string]string),
readbackModels: make(map[string]struct{}, len(cfg.ReadbackModels)),
}
for _, m := range cfg.ReadbackModels {
@@ -124,12 +123,6 @@ func (c *Client) NeedReadback(model string) bool {
return ok
}
// SetLogger 设置日志记录器。
func (c *Client) SetLogger(l logger.Logger) { c.lgr = l }
// Logger 返回日志记录器。
func (c *Client) Logger() logger.Logger { return c.lgr }
// SetHomeIDs 动态设置 miot 层的家庭过滤,供后台定时 RefreshDevices 使用。
func (c *Client) SetHomeIDs(homeIDs []string) { c.inner.SetHomeIDs(homeIDs) }
@@ -174,7 +167,7 @@ func (c *Client) getOrCreateDevice(did string, info map[string]interface{}) *mio
if err == nil {
spec = s
} else {
c.lgr.Warnf("[xiaomi] spec parse failed for %s (%s): %v", did, urn, err)
logrus.Warnf("[xiaomi] spec parse failed for %s (%s): %v", did, urn, err)
}
}
+2 -2
View File
@@ -4,8 +4,8 @@ package xiaomi
type AuthInfo struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
ExpiresIn int32 `json:"expires_in"` // 有效期(秒)
ExpiresTS int64 `json:"expires_ts"` // 过期时间戳(unix),0 则内部按 expires_in 计算
ExpiresIn int32 `json:"expires_in"` // 有效期(秒)
ExpiresTS int64 `json:"expires_ts"` // 过期时间戳(unix),0 则内部按 expires_in 计算
}
// Config 客户端初始化配置。
+4 -4
View File
@@ -4,10 +4,10 @@ import "testing"
func TestStrVal(t *testing.T) {
tests := []struct {
name string
m map[string]interface{}
key string
want string
name string
m map[string]interface{}
key string
want string
}{
{"string value", map[string]interface{}{"name": "test"}, "name", "test"},
{"missing key", map[string]interface{}{"name": "test"}, "nonexistent", ""},
+43 -51
View File
@@ -9,6 +9,8 @@ import (
"xiaomihome/miot"
"xiaomihome/xiaomi"
"github.com/sirupsen/logrus"
)
// valMapper maps between device raw values and semantic names. Built from SPEC ValueList.
@@ -36,8 +38,8 @@ func newValMapper(vl miot.MIoTSpecValueList) *valMapper {
display = item.Name
}
key := strings.ToLower(display)
vm.val2name[v] = display // 原始描述做展示
vm.name2val[key] = v // 小写 key 做匹配
vm.val2name[v] = display // 原始描述做展示
vm.name2val[key] = v // 小写 key 做匹配
vm.names = append(vm.names, display)
}
return vm
@@ -122,7 +124,9 @@ func (vm *valMapper) levelValues() []int {
}
func (vm *valMapper) allNames() []string {
if vm == nil { return nil }
if vm == nil {
return nil
}
return vm.names
}
@@ -207,7 +211,7 @@ type AirConditioner interface {
// 功率
HasPower() bool
GetElectricPower(ctx context.Context) (float64, error) // 当前电功率(W)
GetElectricPower(ctx context.Context) (float64, error) // 当前电功率(W)
GetPowerConsumption(ctx context.Context) (float64, error) // 累计耗电量(kWh)
// 订阅
@@ -218,47 +222,47 @@ type AirConditioner interface {
// ACProps 空调完整状态(字段名对齐 SPEC type)
type ACProps struct {
On bool `json:"on"`
Mode string `json:"mode"` // SPEC ValueList 解析后的名称:cool/heat/fan/dry/auto
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
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"
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
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 推送维护的本地缓存
// set 后回读(验证设置是否成功):记录目标值,防抖 + 重试比较
rbMu sync.Mutex
rbPending bool // 是否有回读调度在跑
rbLastSet time.Time // 最近一次 set 时间
rbTargets []rbTarget // 本次待验证的目标(聚合连续 set)
rbHandler func(props ACProps, changed string) // OnPropsChanged 注册的 handler
rbPending bool // 是否有回读调度在跑
rbLastSet time.Time // 最近一次 set 时间
rbTargets []rbTarget // 本次待验证的目标(聚合连续 set)
rbHandler func(props ACProps, changed string) // OnPropsChanged 注册的 handler
}
// rbTarget 回读验证目标:某属性本次 set 的目标值。
@@ -729,9 +733,7 @@ func (d *acDevice) recordReadback(name string, want interface{}) {
}
d.rbPending = true
d.rbMu.Unlock()
if d.client.Logger() != nil {
d.client.Logger().Debugf("[acDevice] readback scheduled: %s (%s) targets=%v\n", d.info.DID, d.info.Model, d.rbTargetNames())
}
logrus.Debugf("[acDevice] readback scheduled: %s (%s) targets=%v\n", d.info.DID, d.info.Model, d.rbTargetNames())
go d.readbackLoop()
}
@@ -768,9 +770,7 @@ func (d *acDevice) readbackLoop() {
}
props, err := d.FetchProps(ctx)
if err != nil {
if d.client.Logger() != nil {
d.client.Logger().Warnf("[acDevice] readback %s attempt %d failed: %v\n", d.info.DID, attempt, err)
}
logrus.Warnf("[acDevice] readback %s attempt %d failed: %v\n", d.info.DID, attempt, err)
if ctx.Err() != nil {
d.finishReadback("timeout")
return
@@ -782,9 +782,7 @@ func (d *acDevice) readbackLoop() {
d.finishReadbackOK(props, targets)
return
}
if d.client.Logger() != nil {
d.client.Logger().Debugf("[acDevice] readback %s attempt %d not applied yet: missed=%v\n", d.info.DID, attempt, missed)
}
logrus.Debugf("[acDevice] readback %s attempt %d not applied yet: missed=%v\n", d.info.DID, attempt, missed)
if attempt >= readbackMaxRetries {
d.finishReadback("not applied after retries")
return
@@ -810,9 +808,7 @@ func (d *acDevice) finishReadbackOK(props ACProps, targets []rbTarget) {
handler := d.rbHandler
d.rbMu.Unlock()
if handler == nil {
if d.client.Logger() != nil {
d.client.Logger().Warnf("[acDevice] readback %s done but handler not registered (OnPropsChanged never called)\n", d.info.DID)
}
logrus.Warnf("[acDevice] readback %s done but handler not registered (OnPropsChanged never called)\n", d.info.DID)
d.rbMu.Lock()
d.rbPending = false
d.rbMu.Unlock()
@@ -821,11 +817,9 @@ func (d *acDevice) finishReadbackOK(props ACProps, targets []rbTarget) {
for _, t := range targets {
handler(props, t.name)
}
if d.client.Logger() != nil {
d.client.Logger().Debugf("[acDevice] readback %s success: %v on=%v mode=%s target=%.0f temp=%.0f fan=%s hum=%d\n",
d.info.DID, d.rbTargetNamesLocked(), props.On, props.Mode, props.TargetTemperature,
props.Temperature, props.FanLevel, props.CurrentHumidity)
}
logrus.Debugf("[acDevice] readback %s success: %v on=%v mode=%s target=%.0f temp=%.0f fan=%s hum=%d\n",
d.info.DID, d.rbTargetNamesLocked(), props.On, props.Mode, props.TargetTemperature,
props.Temperature, props.FanLevel, props.CurrentHumidity)
d.rbMu.Lock()
d.rbPending = false
d.rbMu.Unlock()
@@ -838,9 +832,7 @@ func (d *acDevice) finishReadback(reason string) {
d.rbTargets = nil
d.rbPending = false
d.rbMu.Unlock()
if d.client.Logger() != nil {
d.client.Logger().Warnf("[acDevice] readback %s finished: %s targets=%v\n", d.info.DID, reason, names)
}
logrus.Warnf("[acDevice] readback %s finished: %s targets=%v\n", d.info.DID, reason, names)
}
func (d *acDevice) rbTargetNames() []string {
+24 -8
View File
@@ -71,15 +71,21 @@ func NewCover(client *xiaomi.Client, info *xiaomi.DeviceInfo) (Cover, error) {
}
func (d *coverDevice) Open(ctx context.Context) error {
if !d.propMotor.valid { return xiaomi.ErrNotSupported }
if !d.propMotor.valid {
return xiaomi.ErrNotSupported
}
return d.SetProp(ctx, d.propMotor.siid, d.propMotor.piid, coverMotorOpen)
}
func (d *coverDevice) Close(ctx context.Context) error {
if !d.propMotor.valid { return xiaomi.ErrNotSupported }
if !d.propMotor.valid {
return xiaomi.ErrNotSupported
}
return d.SetProp(ctx, d.propMotor.siid, d.propMotor.piid, coverMotorClose)
}
func (d *coverDevice) Stop(ctx context.Context) error {
if !d.propMotor.valid { return xiaomi.ErrNotSupported }
if !d.propMotor.valid {
return xiaomi.ErrNotSupported
}
return d.SetProp(ctx, d.propMotor.siid, d.propMotor.piid, coverMotorStop)
}
@@ -96,9 +102,13 @@ func (d *coverDevice) SetPosition(ctx context.Context, pct int) error {
}
func (d *coverDevice) GetPosition(ctx context.Context) (int, error) {
if !d.propPosition.valid { return 0, xiaomi.ErrNotSupported }
if !d.propPosition.valid {
return 0, xiaomi.ErrNotSupported
}
prop, err := d.GetProp(ctx, d.propPosition.siid, d.propPosition.piid)
if err != nil { return 0, err }
if err != nil {
return 0, err
}
return toInt(prop.Value), nil
}
@@ -125,9 +135,15 @@ func (d *coverDevice) GetProps() CoverProps { return d.propsCache }
func (d *coverDevice) FetchProps(ctx context.Context) (CoverProps, error) {
keys := []xiaomi.PropKey{}
if d.propPosition.valid { keys = append(keys, xiaomi.PropKey{DID: d.info.DID, SIID: d.propPosition.siid, PIID: d.propPosition.piid}) }
if d.propStatus.valid { keys = append(keys, xiaomi.PropKey{DID: d.info.DID, SIID: d.propStatus.siid, PIID: d.propStatus.piid}) }
if d.propFault.valid { keys = append(keys, xiaomi.PropKey{DID: d.info.DID, SIID: d.propFault.siid, PIID: d.propFault.piid}) }
if d.propPosition.valid {
keys = append(keys, xiaomi.PropKey{DID: d.info.DID, SIID: d.propPosition.siid, PIID: d.propPosition.piid})
}
if d.propStatus.valid {
keys = append(keys, xiaomi.PropKey{DID: d.info.DID, SIID: d.propStatus.siid, PIID: d.propStatus.piid})
}
if d.propFault.valid {
keys = append(keys, xiaomi.PropKey{DID: d.info.DID, SIID: d.propFault.siid, PIID: d.propFault.piid})
}
if len(keys) == 0 {
return d.propsCache, xiaomi.ErrNotSupported
}
+9 -2
View File
@@ -209,7 +209,11 @@ func TestValMapperLevelFallback(t *testing.T) {
{Value: 3, Desc: "Level3"}, {Value: 4, Desc: "Level4"}, {Value: 5, Desc: "Level5"},
}
vm := newValMapperLevel(vl)
cases := []struct{ name string; want int; ok bool }{
cases := []struct {
name string
want int
ok bool
}{
{"auto", 0, true}, {"Auto", 0, true},
{"low", 1, true}, {"medium", 3, true}, {"high", 5, true},
{"level3", 3, true},
@@ -221,7 +225,10 @@ func TestValMapperLevelFallback(t *testing.T) {
}
}
// 反向解析(MQTT 上报值 → 语义枚举),与 val() 正向对称
rname := []struct{ v int; want string }{
rname := []struct {
v int
want string
}{
{0, "auto"}, {1, "low"}, {3, "medium"}, {5, "high"}, {2, "Level2"},
}
for _, c := range rname {
+2 -2
View File
@@ -55,8 +55,8 @@ type fanDevice struct {
propNaturalWind siidPiid // "mode" — 无 ValueList 时为自然风开关
propOscillation siidPiid // "horizontal-swing" or "oscillation"
modeValues []int // 可用模式值列表(来自 SPEC value-list)
fanLevelValues []int // 可用风速档位值列表
modeValues []int // 可用模式值列表(来自 SPEC value-list)
fanLevelValues []int // 可用风速档位值列表
propsCache FanProps
}
+71 -24
View File
@@ -39,53 +39,87 @@ type humidifierDevice struct {
func NewHumidifier(client *xiaomi.Client, info *xiaomi.DeviceInfo) (Humidifier, error) {
d := &humidifierDevice{BaseDevice: NewBaseDevice(client, info)}
resolver := d.getSpecResolver()
if resolver == nil { return nil, xiaomi.ErrNoSPEC }
if resolver == nil {
return nil, xiaomi.ErrNoSPEC
}
siid, piid, ok := resolver.find("on")
if !ok { return nil, xiaomi.ErrNotSupported }
if !ok {
return nil, xiaomi.ErrNotSupported
}
d.propOn = siidPiid{siid, piid, true}
if s, p, ok := resolver.find("target-humidity"); ok { d.propTargetHumidity = siidPiid{s, p, true} }
if s, p, ok := resolver.find("fan-level"); ok { d.propFanLevel = siidPiid{s, p, true} }
if s, p, ok := resolver.find("target-humidity"); ok {
d.propTargetHumidity = siidPiid{s, p, true}
}
if s, p, ok := resolver.find("fan-level"); ok {
d.propFanLevel = siidPiid{s, p, true}
}
return d, nil
}
func (d *humidifierDevice) TurnOn(ctx context.Context) error {
if !d.propOn.valid { return xiaomi.ErrNotSupported }
if !d.propOn.valid {
return xiaomi.ErrNotSupported
}
return d.SetProp(ctx, d.propOn.siid, d.propOn.piid, true)
}
func (d *humidifierDevice) TurnOff(ctx context.Context) error {
if !d.propOn.valid { return xiaomi.ErrNotSupported }
if !d.propOn.valid {
return xiaomi.ErrNotSupported
}
return d.SetProp(ctx, d.propOn.siid, d.propOn.piid, false)
}
func (d *humidifierDevice) IsOn(ctx context.Context) (bool, error) {
if !d.propOn.valid { return false, xiaomi.ErrNotSupported }
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 }
if err != nil {
return false, err
}
if b, ok := prop.Value.(bool); ok {
return b, nil
}
return false, nil
}
func (d *humidifierDevice) SetTargetHumidity(ctx context.Context, pct int) error {
if !d.propTargetHumidity.valid { return xiaomi.ErrNotSupported }
if !d.propTargetHumidity.valid {
return xiaomi.ErrNotSupported
}
return d.SetProp(ctx, d.propTargetHumidity.siid, d.propTargetHumidity.piid, pct)
}
func (d *humidifierDevice) GetTargetHumidity(ctx context.Context) (int, error) {
if !d.propTargetHumidity.valid { return 0, xiaomi.ErrNotSupported }
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 }
if err != nil {
return 0, err
}
return toInt(prop.Value), nil
}
func (d *humidifierDevice) SetFanLevel(ctx context.Context, level int) error {
if !d.propFanLevel.valid { return xiaomi.ErrNotSupported }
if !d.propFanLevel.valid {
return xiaomi.ErrNotSupported
}
return d.SetProp(ctx, d.propFanLevel.siid, d.propFanLevel.piid, level)
}
func (d *humidifierDevice) GetFanLevel(ctx context.Context) (int, error) {
if !d.propFanLevel.valid { return 0, xiaomi.ErrNotSupported }
if !d.propFanLevel.valid {
return 0, xiaomi.ErrNotSupported
}
prop, err := d.GetProp(ctx, d.propFanLevel.siid, d.propFanLevel.piid)
if err != nil { return 0, err }
if err != nil {
return 0, err
}
return toInt(prop.Value), nil
}
func (d *humidifierDevice) OnPropsChanged(handler func(props HumidifierProps, changed string)) (string, error) {
type field struct{ siid, piid int; name string; fill func(*HumidifierProps, interface{}) }
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 *HumidifierProps, v interface{}) { s.On, _ = v.(bool) }})
@@ -96,7 +130,9 @@ func (d *humidifierDevice) OnPropsChanged(handler func(props HumidifierProps, ch
if d.propFanLevel.valid {
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 }
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 {
@@ -111,17 +147,28 @@ func (d *humidifierDevice) OnPropsChanged(handler func(props HumidifierProps, ch
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}) }
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.propsCache, err }
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: 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)
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.propsCache = result
+29 -13
View File
@@ -38,10 +38,10 @@ type Light interface {
// 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"
ColorRGB [3]int `json:"colorRgb"` // SPEC: "color"
On bool `json:"on"` // SPEC: "on"
Brightness int `json:"brightness"` // SPEC: "brightness"
ColorTemperature int `json:"colorTemperature"` // SPEC: "color-temperature"
ColorRGB [3]int `json:"colorRgb"` // SPEC: "color"
}
type lightDevice struct {
@@ -252,20 +252,36 @@ func toInt(v interface{}) int {
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}) }
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.propsCache, err }
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: 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.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); result.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.propsCache = result
+40 -16
View File
@@ -20,9 +20,9 @@ type OccupancySensor interface {
type occupancyDevice struct {
BaseDevice
propOccupancy siidPiid // "occupancy" / "occupancy-status"(bool/uint8)
propNoMotion siidPiid // "no-motion-duration"(uint16,linp.motion.* 等无 occupancy 属性的设备)
propIlluminance siidPiid // "illumination"
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
}
@@ -35,21 +35,33 @@ func (d *occupancyDevice) occupied(v interface{}) bool {
}
func NewOccupancySensor(client *xiaomi.Client, info *xiaomi.DeviceInfo) (OccupancySensor, error) {
if info == nil { return nil, xiaomi.ErrDeviceNotFound }
if info == nil {
return nil, xiaomi.ErrDeviceNotFound
}
miotDev := client.GetMIoTDevice(info.DID)
if miotDev == nil { return nil, fmt.Errorf("%w: device not initialized", xiaomi.ErrDeviceNotFound) }
if miotDev == nil {
return nil, fmt.Errorf("%w: device not initialized", xiaomi.ErrDeviceNotFound)
}
spec := miotDev.SpecInstance()
if spec == nil { return nil, fmt.Errorf("%w: device has no SPEC information", xiaomi.ErrNotSupported) }
if spec == nil {
return nil, fmt.Errorf("%w: device has no SPEC information", xiaomi.ErrNotSupported)
}
d := &occupancyDevice{BaseDevice: BaseDevice{client: client, info: info}}
for _, svc := range spec.Services {
for _, prop := range svc.Properties {
switch propertyShortName(prop.Type) {
case "occupancy", "occupancy-status":
if !d.propOccupancy.valid { d.propOccupancy = siidPiid{svc.IID, prop.PIID, true} }
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} }
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.propIlluminance.valid {
d.propIlluminance = siidPiid{svc.IID, prop.PIID, true}
}
}
}
}
@@ -70,18 +82,26 @@ func (d *occupancyDevice) occupiedProp() siidPiid {
func (d *occupancyDevice) IsOccupied(ctx context.Context) (bool, error) {
p := d.occupiedProp()
prop, err := d.GetProp(ctx, p.siid, p.piid)
if err != nil { return false, err }
if err != nil {
return false, err
}
return d.occupied(prop.Value), nil
}
func (d *occupancyDevice) GetIlluminance(ctx context.Context) (float64, error) {
if !d.propIlluminance.valid { return 0, xiaomi.ErrNotSupported }
if !d.propIlluminance.valid {
return 0, xiaomi.ErrNotSupported
}
prop, err := d.GetProp(ctx, d.propIlluminance.siid, d.propIlluminance.piid)
if err != nil { return 0, err }
if err != nil {
return 0, err
}
return toFloat(prop.Value), nil
}
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 }
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:
@@ -97,7 +117,7 @@ func (d *occupancyDevice) OnPropsChanged(handler func(occupied bool, illuminance
})
}
func (d *occupancyDevice) GetProps() (bool, float64) {
func (d *occupancyDevice) GetProps() (bool, float64) {
b, _ := d.propsCache[0].(bool)
f, _ := d.propsCache[1].(float64)
return b, f
@@ -105,8 +125,12 @@ func (d *occupancyDevice) GetProps() (bool, float64) {
func (d *occupancyDevice) FetchProps(ctx context.Context) (bool, float64, error) {
keys := []xiaomi.PropKey{}
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}) }
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.propsCache[0].(bool)
+13 -13
View File
@@ -4,19 +4,19 @@ 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" // 热水器
TypeOccupancySensor = "sensor_occupy" // 人体传感器
TypeTempHumidSensor = "sensor" // 温湿度传感器(官方 URN type)
TypeSpeaker = "speaker" // 音箱
TypeGateway = "gateway" // 网关
TypePlug = "plug" // 插座
TypeAirConditioner = "air_conditioner" // 空调
TypeLight = "light" // 灯
TypeSwitch = "switch" // 开关
TypeFan = "fan" // 风扇
TypeCover = "cover" // 窗帘/遮阳帘
TypeHumidifier = "humidifier" // 加湿器
TypeVacuum = "vacuum" // 吸尘器/扫地机器人
TypeWaterHeater = "water_heater" // 热水器
TypeOccupancySensor = "sensor_occupy" // 人体传感器
TypeTempHumidSensor = "sensor" // 温湿度传感器(官方 URN type)
TypeSpeaker = "speaker" // 音箱
TypeGateway = "gateway" // 网关
TypePlug = "plug" // 插座
)
+4 -4
View File
@@ -80,11 +80,11 @@ type speakerDevice struct {
isSiid int
// 动作
actWakeUp isAction
actPlayRadio isAction
actPlayText isAction
actWakeUp isAction
actPlayRadio isAction
actPlayText isAction
actExecuteTextDirective isAction
actPlayMusic isAction
actPlayMusic isAction
propsCache SpeakerProps
}
+43 -15
View File
@@ -26,19 +26,29 @@ type tempHumidityDevice struct {
}
func NewTempHumiditySensor(client *xiaomi.Client, info *xiaomi.DeviceInfo) (TempHumiditySensor, error) {
if info == nil { return nil, xiaomi.ErrDeviceNotFound }
if info == nil {
return nil, xiaomi.ErrDeviceNotFound
}
miotDev := client.GetMIoTDevice(info.DID)
if miotDev == nil { return nil, fmt.Errorf("%w: device not initialized", xiaomi.ErrDeviceNotFound) }
if miotDev == nil {
return nil, fmt.Errorf("%w: device not initialized", xiaomi.ErrDeviceNotFound)
}
spec := miotDev.SpecInstance()
if spec == nil { return nil, fmt.Errorf("%w: device has no SPEC information", xiaomi.ErrNotSupported) }
if spec == nil {
return nil, fmt.Errorf("%w: device has no SPEC information", xiaomi.ErrNotSupported)
}
d := &tempHumidityDevice{BaseDevice: BaseDevice{client: client, info: info}}
for _, svc := range spec.Services {
for _, prop := range svc.Properties {
switch propertyShortName(prop.Type) {
case "temperature":
if !d.propTemp.valid { d.propTemp = siidPiid{svc.IID, prop.PIID, true} }
if !d.propTemp.valid {
d.propTemp = siidPiid{svc.IID, prop.PIID, true}
}
case "relative-humidity":
if !d.propHumidity.valid { d.propHumidity = siidPiid{svc.IID, prop.PIID, true} }
if !d.propHumidity.valid {
d.propHumidity = siidPiid{svc.IID, prop.PIID, true}
}
}
}
}
@@ -49,20 +59,30 @@ func NewTempHumiditySensor(client *xiaomi.Client, info *xiaomi.DeviceInfo) (Temp
}
func (d *tempHumidityDevice) GetTemperature(ctx context.Context) (float64, error) {
if !d.propTemp.valid { return 0, xiaomi.ErrNotSupported }
if !d.propTemp.valid {
return 0, xiaomi.ErrNotSupported
}
prop, err := d.GetProp(ctx, d.propTemp.siid, d.propTemp.piid)
if err != nil { return 0, err }
if err != nil {
return 0, err
}
return toFloat(prop.Value), nil
}
func (d *tempHumidityDevice) GetHumidity(ctx context.Context) (float64, error) {
if !d.propHumidity.valid { return 0, xiaomi.ErrNotSupported }
if !d.propHumidity.valid {
return 0, xiaomi.ErrNotSupported
}
prop, err := d.GetProp(ctx, d.propHumidity.siid, d.propHumidity.piid)
if err != nil { return 0, err }
if err != nil {
return 0, err
}
return toFloat(prop.Value), nil
}
func (d *tempHumidityDevice) OnPropsChanged(handler func(temp, humidity float64)) (string, error) {
if !d.propTemp.valid && !d.propHumidity.valid { return "", xiaomi.ErrNotSupported }
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:
@@ -77,15 +97,23 @@ func (d *tempHumidityDevice) OnPropsChanged(handler func(temp, humidity float64)
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}) }
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.propsCache[0], d.propsCache[1], err }
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)
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.propsCache = state
+83 -28
View File
@@ -34,61 +34,103 @@ type thermostatDevice struct {
func NewThermostat(client *xiaomi.Client, info *xiaomi.DeviceInfo) (Thermostat, error) {
d := &thermostatDevice{BaseDevice: NewBaseDevice(client, info)}
resolver := d.getSpecResolver()
if resolver == nil { return nil, xiaomi.ErrNoSPEC }
if resolver == nil {
return nil, xiaomi.ErrNoSPEC
}
siid, piid, ok := resolver.find("on")
if !ok { return nil, xiaomi.ErrNotSupported }
if !ok {
return nil, xiaomi.ErrNotSupported
}
d.propOn = siidPiid{siid, piid, true}
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} }
if s, p, ok := resolver.find("heater"); ok { d.propHeating = siidPiid{s, p, true} }
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}
}
if s, p, ok := resolver.find("heater"); ok {
d.propHeating = siidPiid{s, p, true}
}
return d, nil
}
func (d *thermostatDevice) TurnOn(ctx context.Context) error {
if !d.propOn.valid { return xiaomi.ErrNotSupported }
if !d.propOn.valid {
return xiaomi.ErrNotSupported
}
return d.SetProp(ctx, d.propOn.siid, d.propOn.piid, true)
}
func (d *thermostatDevice) TurnOff(ctx context.Context) error {
if !d.propOn.valid { return xiaomi.ErrNotSupported }
if !d.propOn.valid {
return xiaomi.ErrNotSupported
}
return d.SetProp(ctx, d.propOn.siid, d.propOn.piid, false)
}
func (d *thermostatDevice) IsOn(ctx context.Context) (bool, error) {
if !d.propOn.valid { return false, xiaomi.ErrNotSupported }
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 }
if err != nil {
return false, err
}
if b, ok := prop.Value.(bool); ok {
return b, nil
}
return false, nil
}
func (d *thermostatDevice) SetTargetTemp(ctx context.Context, temp float64) error {
if !d.propTargetTemp.valid { return xiaomi.ErrNotSupported }
if !d.propTargetTemp.valid {
return xiaomi.ErrNotSupported
}
return d.SetProp(ctx, d.propTargetTemp.siid, d.propTargetTemp.piid, temp)
}
func (d *thermostatDevice) GetTargetTemp(ctx context.Context) (float64, error) {
if !d.propTargetTemp.valid { return 0, xiaomi.ErrNotSupported }
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 }
if err != nil {
return 0, err
}
return toFloat(prop.Value), nil
}
func (d *thermostatDevice) GetCurrentTemp(ctx context.Context) (float64, error) {
if !d.propCurrentTemp.valid { return 0, xiaomi.ErrNotSupported }
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 }
if err != nil {
return 0, err
}
return toFloat(prop.Value), nil
}
func (d *thermostatDevice) SetHeating(ctx context.Context, on bool) error {
if !d.propHeating.valid { return xiaomi.ErrNotSupported }
if !d.propHeating.valid {
return xiaomi.ErrNotSupported
}
return d.SetProp(ctx, d.propHeating.siid, d.propHeating.piid, on)
}
func (d *thermostatDevice) IsHeating(ctx context.Context) (bool, error) {
if !d.propHeating.valid { return false, xiaomi.ErrNotSupported }
if !d.propHeating.valid {
return false, xiaomi.ErrNotSupported
}
prop, err := d.GetProp(ctx, d.propHeating.siid, d.propHeating.piid)
if err != nil { return false, err }
if b, ok := prop.Value.(bool); ok { return b, nil }
if err != nil {
return false, err
}
if b, ok := prop.Value.(bool); ok {
return b, nil
}
return false, nil
}
func (d *thermostatDevice) OnPropsChanged(handler func(props HeaterProps, changed string)) (string, error) {
type field struct{ siid, piid int; name string; fill func(*HeaterProps, interface{}) }
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 *HeaterProps, v interface{}) { s.On, _ = v.(bool) }})
@@ -99,7 +141,9 @@ func (d *thermostatDevice) OnPropsChanged(handler func(props HeaterProps, change
if d.propCurrentTemp.valid {
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 }
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 {
@@ -114,17 +158,28 @@ func (d *thermostatDevice) OnPropsChanged(handler func(props HeaterProps, change
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}) }
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.propsCache, err }
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: 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)
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.propsCache = result
+45 -14
View File
@@ -40,13 +40,23 @@ type vacuumDevice struct {
func NewVacuum(client *xiaomi.Client, info *xiaomi.DeviceInfo) (Vacuum, error) {
d := &vacuumDevice{BaseDevice: NewBaseDevice(client, info)}
resolver := d.getSpecResolver()
if resolver == nil { return nil, xiaomi.ErrNoSPEC }
if resolver == nil {
return nil, xiaomi.ErrNoSPEC
}
siid, piid, ok := resolver.find("on")
if !ok { return nil, xiaomi.ErrNotSupported }
if !ok {
return nil, xiaomi.ErrNotSupported
}
d.propOn = siidPiid{siid, piid, true}
if s, p, ok := resolver.find("mode"); ok { d.propMode = siidPiid{s, p, true} }
if s, p, ok := resolver.find("fan-level"); ok { d.propFanLevel = siidPiid{s, p, true} }
if s, p, ok := resolver.find("battery-level"); ok { d.propBattery = siidPiid{s, p, true} }
if s, p, ok := resolver.find("mode"); ok {
d.propMode = siidPiid{s, p, true}
}
if s, p, ok := resolver.find("fan-level"); ok {
d.propFanLevel = siidPiid{s, p, true}
}
if s, p, ok := resolver.find("battery-level"); ok {
d.propBattery = siidPiid{s, p, true}
}
return d, nil
}
@@ -63,24 +73,37 @@ func (d *vacuumDevice) ReturnToDock(ctx context.Context) error {
return err
}
func (d *vacuumDevice) SetFanLevel(ctx context.Context, level int) error {
if !d.propFanLevel.valid { return xiaomi.ErrNotSupported }
if !d.propFanLevel.valid {
return xiaomi.ErrNotSupported
}
return d.SetProp(ctx, d.propFanLevel.siid, d.propFanLevel.piid, level)
}
func (d *vacuumDevice) GetFanLevel(ctx context.Context) (int, error) {
if !d.propFanLevel.valid { return 0, xiaomi.ErrNotSupported }
if !d.propFanLevel.valid {
return 0, xiaomi.ErrNotSupported
}
prop, err := d.GetProp(ctx, d.propFanLevel.siid, d.propFanLevel.piid)
if err != nil { return 0, err }
if err != nil {
return 0, err
}
return toInt(prop.Value), nil
}
func (d *vacuumDevice) GetStatus(ctx context.Context) (VacuumProps, 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}) }
if sp.valid {
keys = append(keys, xiaomi.PropKey{DID: d.info.DID, SIID: sp.siid, PIID: sp.piid})
}
}
add(d.propOn); add(d.propMode); add(d.propFanLevel); add(d.propBattery)
add(d.propOn)
add(d.propMode)
add(d.propFanLevel)
add(d.propBattery)
props, err := d.client.GetProps(ctx, keys)
if err != nil { return VacuumProps{}, err }
if err != nil {
return VacuumProps{}, err
}
status := VacuumProps{}
for _, p := range props {
switch {
@@ -98,7 +121,11 @@ func (d *vacuumDevice) GetStatus(ctx context.Context) (VacuumProps, error) {
}
func (d *vacuumDevice) OnPropsChanged(handler func(props VacuumProps, changed string)) (string, error) {
type field struct{ siid, piid int; name string; fill func(*VacuumProps, interface{}) }
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 *VacuumProps, v interface{}) { s.On, _ = v.(bool) }})
@@ -112,7 +139,9 @@ func (d *vacuumDevice) OnPropsChanged(handler func(props VacuumProps, changed st
if d.propBattery.valid {
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 }
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 {
@@ -127,7 +156,9 @@ func (d *vacuumDevice) OnPropsChanged(handler func(props VacuumProps, changed st
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.propsCache, err }
if err != nil {
return d.propsCache, err
}
d.propsCache = status
return status, nil
}
+68 -23
View File
@@ -38,49 +38,81 @@ type waterHeaterDevice struct {
func NewWaterHeater(client *xiaomi.Client, info *xiaomi.DeviceInfo) (WaterHeater, error) {
d := &waterHeaterDevice{BaseDevice: NewBaseDevice(client, info)}
resolver := d.getSpecResolver()
if resolver == nil { return nil, xiaomi.ErrNoSPEC }
if resolver == nil {
return nil, xiaomi.ErrNoSPEC
}
siid, piid, ok := resolver.find("on")
if !ok { return nil, xiaomi.ErrNotSupported }
if !ok {
return nil, xiaomi.ErrNotSupported
}
d.propOn = siidPiid{siid, piid, true}
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} }
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}
}
return d, nil
}
func (d *waterHeaterDevice) TurnOn(ctx context.Context) error {
if !d.propOn.valid { return xiaomi.ErrNotSupported }
if !d.propOn.valid {
return xiaomi.ErrNotSupported
}
return d.SetProp(ctx, d.propOn.siid, d.propOn.piid, true)
}
func (d *waterHeaterDevice) TurnOff(ctx context.Context) error {
if !d.propOn.valid { return xiaomi.ErrNotSupported }
if !d.propOn.valid {
return xiaomi.ErrNotSupported
}
return d.SetProp(ctx, d.propOn.siid, d.propOn.piid, false)
}
func (d *waterHeaterDevice) IsOn(ctx context.Context) (bool, error) {
if !d.propOn.valid { return false, xiaomi.ErrNotSupported }
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 }
if err != nil {
return false, err
}
if b, ok := prop.Value.(bool); ok {
return b, nil
}
return false, nil
}
func (d *waterHeaterDevice) SetTargetTemp(ctx context.Context, temp float64) error {
if !d.propTargetTemp.valid { return xiaomi.ErrNotSupported }
if !d.propTargetTemp.valid {
return xiaomi.ErrNotSupported
}
return d.SetProp(ctx, d.propTargetTemp.siid, d.propTargetTemp.piid, temp)
}
func (d *waterHeaterDevice) GetTargetTemp(ctx context.Context) (float64, error) {
if !d.propTargetTemp.valid { return 0, xiaomi.ErrNotSupported }
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 }
if err != nil {
return 0, err
}
return toFloat(prop.Value), nil
}
func (d *waterHeaterDevice) GetCurrentTemp(ctx context.Context) (float64, error) {
if !d.propCurrentTemp.valid { return 0, xiaomi.ErrNotSupported }
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 }
if err != nil {
return 0, err
}
return toFloat(prop.Value), nil
}
func (d *waterHeaterDevice) OnPropsChanged(handler func(props HeaterProps, changed string)) (string, error) {
type field struct{ siid, piid int; name string; fill func(*HeaterProps, interface{}) }
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 *HeaterProps, v interface{}) { s.On, _ = v.(bool) }})
@@ -91,7 +123,9 @@ func (d *waterHeaterDevice) OnPropsChanged(handler func(props HeaterProps, chang
if d.propCurrentTemp.valid {
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 }
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 {
@@ -106,17 +140,28 @@ func (d *waterHeaterDevice) OnPropsChanged(handler func(props HeaterProps, chang
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}) }
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.propsCache, err }
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: 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)
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.propsCache = result
+12 -4
View File
@@ -22,9 +22,13 @@ type TokenResult struct {
// AuthURL 生成小米 OAuth 授权页面 URL。
func AuthURL(cfg Config) string {
uuid := cfg.UUID
if uuid == "" { uuid = generateUUID() }
if uuid == "" {
uuid = generateUUID()
}
redirectURL := cfg.RedirectURL
if redirectURL == "" { redirectURL = miot.OAUTH_REDIRECT_URL }
if redirectURL == "" {
redirectURL = miot.OAUTH_REDIRECT_URL
}
oa := miot.NewMIoTOauthClient(miot.OAUTH2_CLIENT_ID, redirectURL, "cn", uuid)
return oa.GenAuthURL("", "", nil, false)
}
@@ -32,9 +36,13 @@ func AuthURL(cfg Config) string {
// ExchangeCode 用 authorization code 交换 access token。
func ExchangeCode(ctx context.Context, cfg Config, code string) (*TokenResult, error) {
uuid := cfg.UUID
if uuid == "" { uuid = generateUUID() }
if uuid == "" {
uuid = generateUUID()
}
redirectURL := cfg.RedirectURL
if redirectURL == "" { redirectURL = miot.OAUTH_REDIRECT_URL }
if redirectURL == "" {
redirectURL = miot.OAUTH_REDIRECT_URL
}
oa := miot.NewMIoTOauthClient(miot.OAUTH2_CLIENT_ID, redirectURL, "cn", uuid)
result, err := oa.GetAccessToken(code)
if err != nil {
+3 -3
View File
@@ -17,10 +17,10 @@ import (
// description (translated), and data format for flexible lookup.
type PropertyResolver struct {
spec *miot.MIoTSpecInstance
byType map[string]*miot.MIoTSpecProperty // type → first matching property
byDesc map[string]*miot.MIoTSpecProperty // description → property
byType map[string]*miot.MIoTSpecProperty // type → first matching property
byDesc map[string]*miot.MIoTSpecProperty // description → property
byFormat map[string][]*miot.MIoTSpecProperty // format → properties
actionMap map[int]map[string]int // siid → (actionType → aiid)
actionMap map[int]map[string]int // siid → (actionType → aiid)
}
// NewPropertyResolver creates a PropertyResolver from a SPEC instance.
+14 -14
View File
@@ -146,20 +146,20 @@ type DeviceStateHandler func(did string, state DeviceState)
type Kind string
const (
KindAirConditioner Kind = "air_conditioner"
KindLight Kind = "light"
KindSwitch Kind = "switch"
KindFan Kind = "fan"
KindCover Kind = "cover"
KindHumidifier Kind = "humidifier"
KindVacuum Kind = "vacuum"
KindWaterHeater Kind = "water_heater"
KindSpeaker Kind = "speaker"
KindGateway Kind = "gateway"
KindPlug Kind = "plug"
KindOccupancySensor Kind = "sensor_occupy"
KindSensor Kind = "sensor"
KindUnknown Kind = "unknown"
KindAirConditioner Kind = "air_conditioner"
KindLight Kind = "light"
KindSwitch Kind = "switch"
KindFan Kind = "fan"
KindCover Kind = "cover"
KindHumidifier Kind = "humidifier"
KindVacuum Kind = "vacuum"
KindWaterHeater Kind = "water_heater"
KindSpeaker Kind = "speaker"
KindGateway Kind = "gateway"
KindPlug Kind = "plug"
KindOccupancySensor Kind = "sensor_occupy"
KindSensor Kind = "sensor"
KindUnknown Kind = "unknown"
)
// DeviceKind 从 URN 快速判断设备种类(不依赖 SPEC 加载)
+2 -2
View File
@@ -14,11 +14,11 @@ func TestDeviceInfoParsing(t *testing.T) {
"icon": "https://example.com/icon.png",
"urn": "urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-c1:1",
"token": "abc123token",
"local_ip": "192.168.1.100",
"local_ip": "192.168.1.100",
"rssi": float64(-55),
"ssid": "MyWiFi",
"bssid": "AA:BB:CC:DD:EE:FF",
"online": true,
"online": true,
"home_id": "home1",
"home_name": "我的家",
"room_id": "room1",