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:
+59
-59
@@ -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
@@ -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
@@ -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,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", ""},
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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" // 插座
|
||||
|
||||
)
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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 加载)
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user