Files
xiaomihome/xiaomi/devices/fan.go
T
4566704 b808e51b3e refactor: 重大架构升级 — HomeID/RoomID int64化 + 设备驱动重构 + 结构化日志
主要变更:

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

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

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

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

**其他**
- Config 新增 RedirectURL 和 HomeIDs 字段
- AuthInfo.ExpiresIn 类型从 int 改为 int32
- UUID 生成简化为 hex.EncodeToString
- 新增 github.com/sirupsen/logrus 依赖
- .gitignore 新增 examples/*/data/ 忽略规则
- 删除已提交的 config.json 敏感配置
2026-07-13 10:40:20 +08:00

220 lines
6.7 KiB
Go

package devices
import (
"context"
"xiaomihome/xiaomi"
)
// Fan 风扇统一控制接口
type Fan interface {
Device
TurnOn(ctx context.Context) error
TurnOff(ctx context.Context) error
IsOn(ctx context.Context) (bool, error)
SetFanLevel(ctx context.Context, level int) error // 0=off, 1-4 档位
GetFanLevel(ctx context.Context) (int, error)
SetOscillation(ctx context.Context, on bool) error
IsOscillating(ctx context.Context) (bool, error)
SetNaturalWind(ctx context.Context, on bool) error
IsNaturalWind(ctx context.Context) (bool, error)
OnStateChanged(handler func(state FanState, changed string)) (string, error)
GetState() FanState
FetchState(ctx context.Context) (FanState, error)
}
// FanState 风扇完整状态(MQTT 缓存)
type FanState struct {
On bool `json:"on"`
FanLevel int `json:"fanLevel"`
Oscillation bool `json:"oscillation"`
NaturalWind bool `json:"naturalWind"`
}
type fanDevice struct {
BaseDevice
propOn siidPiid // "on"
propFanLevel siidPiid // "fan-level"
propOscillation siidPiid // "horizontal-swing" or "oscillation"
propNaturalWind siidPiid // "mode" (natural wind mode)
stateCache FanState
}
// NewFan creates a Fan control for the given device.
func NewFan(client *xiaomi.Client, info *xiaomi.DeviceInfo) (Fan, error) {
d := &fanDevice{
BaseDevice: NewBaseDevice(client, info),
}
resolver := d.getSpecResolver()
if resolver == nil {
return nil, xiaomi.ErrNoSPEC
}
siid, piid, ok := resolver.find("on")
if !ok {
return nil, xiaomi.ErrNotSupported
}
d.propOn = siidPiid{siid, piid, true}
if s, p, ok := resolver.find("fan-level"); ok {
d.propFanLevel = siidPiid{s, p, true}
}
if s, p, ok := resolver.find("horizontal-swing"); ok {
d.propOscillation = siidPiid{s, p, true}
}
if s, p, ok := resolver.find("mode"); ok {
d.propNaturalWind = siidPiid{s, p, true}
}
return d, nil
}
func (d *fanDevice) TurnOn(ctx context.Context) error {
if !d.propOn.valid {
return xiaomi.ErrNotSupported
}
return d.SetProp(ctx, d.propOn.siid, d.propOn.piid, true)
}
func (d *fanDevice) TurnOff(ctx context.Context) error {
if !d.propOn.valid {
return xiaomi.ErrNotSupported
}
return d.SetProp(ctx, d.propOn.siid, d.propOn.piid, false)
}
func (d *fanDevice) IsOn(ctx context.Context) (bool, error) {
if !d.propOn.valid {
return false, xiaomi.ErrNotSupported
}
prop, err := d.GetProp(ctx, d.propOn.siid, d.propOn.piid)
if err != nil {
return false, err
}
if b, ok := prop.Value.(bool); ok {
return b, nil
}
return false, nil
}
func (d *fanDevice) SetFanLevel(ctx context.Context, level int) error {
if !d.propFanLevel.valid {
return xiaomi.ErrNotSupported
}
return d.SetProp(ctx, d.propFanLevel.siid, d.propFanLevel.piid, level)
}
func (d *fanDevice) GetFanLevel(ctx context.Context) (int, error) {
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
}
return toInt(prop.Value), nil
}
func (d *fanDevice) SetOscillation(ctx context.Context, on bool) error {
if !d.propOscillation.valid {
return xiaomi.ErrNotSupported
}
return d.SetProp(ctx, d.propOscillation.siid, d.propOscillation.piid, on)
}
func (d *fanDevice) IsOscillating(ctx context.Context) (bool, error) {
if !d.propOscillation.valid {
return false, xiaomi.ErrNotSupported
}
prop, err := d.GetProp(ctx, d.propOscillation.siid, d.propOscillation.piid)
if err != nil {
return false, err
}
if b, ok := prop.Value.(bool); ok {
return b, nil
}
return false, nil
}
func (d *fanDevice) SetNaturalWind(ctx context.Context, on bool) error {
if !d.propNaturalWind.valid {
return xiaomi.ErrNotSupported
}
mode := 0 // normal
if on {
mode = 1 // natural wind
}
return d.SetProp(ctx, d.propNaturalWind.siid, d.propNaturalWind.piid, mode)
}
func (d *fanDevice) IsNaturalWind(ctx context.Context) (bool, error) {
if !d.propNaturalWind.valid {
return false, xiaomi.ErrNotSupported
}
prop, err := d.GetProp(ctx, d.propNaturalWind.siid, d.propNaturalWind.piid)
if err != nil {
return false, err
}
return toInt(prop.Value) == 1, nil
}
func (d *fanDevice) OnStateChanged(handler func(state FanState, changed string)) (string, error) {
type field struct{ siid, piid int; name string; fill func(*FanState, interface{}) }
var fields []field
if d.propOn.valid {
fields = append(fields, field{d.propOn.siid, d.propOn.piid, "on",
func(s *FanState, v interface{}) { s.On, _ = v.(bool) }})
}
if d.propFanLevel.valid {
fields = append(fields, field{d.propFanLevel.siid, d.propFanLevel.piid, "fan-level",
func(s *FanState, v interface{}) { s.FanLevel = toInt(v) }})
}
if d.propOscillation.valid {
fields = append(fields, field{d.propOscillation.siid, d.propOscillation.piid, "swing",
func(s *FanState, v interface{}) { s.Oscillation, _ = v.(bool) }})
}
if d.propNaturalWind.valid {
fields = append(fields, field{d.propNaturalWind.siid, d.propNaturalWind.piid, "mode",
func(s *FanState, v interface{}) { s.NaturalWind = toInt(v) == 1 }})
}
if len(fields) == 0 {
return "", xiaomi.ErrNotSupported
}
return d.SubProp(0, 0, func(did string, prop *xiaomi.PropertyValue) {
for _, f := range fields {
if f.siid == prop.SIID && f.piid == prop.PIID {
f.fill(&d.stateCache, prop.Value)
handler(d.stateCache, f.name)
return
}
}
})
}
func (d *fanDevice) GetState() FanState { return d.stateCache }
func (d *fanDevice) FetchState(ctx context.Context) (FanState, error) {
keys := []xiaomi.PropKey{}
if d.propOn.valid { keys = append(keys, xiaomi.PropKey{DID: d.info.DID, SIID: d.propOn.siid, PIID: d.propOn.piid}) }
if d.propFanLevel.valid { keys = append(keys, xiaomi.PropKey{DID: d.info.DID, SIID: d.propFanLevel.siid, PIID: d.propFanLevel.piid}) }
if d.propOscillation.valid { keys = append(keys, xiaomi.PropKey{DID: d.info.DID, SIID: d.propOscillation.siid, PIID: d.propOscillation.piid}) }
if d.propNaturalWind.valid { keys = append(keys, xiaomi.PropKey{DID: d.info.DID, SIID: d.propNaturalWind.siid, PIID: d.propNaturalWind.piid}) }
props, err := d.client.GetProps(ctx, keys)
if err != nil { return d.stateCache, err }
state := FanState{}
for _, p := range props {
switch {
case d.propOn.valid && p.SIID == d.propOn.siid && p.PIID == d.propOn.piid: state.On, _ = p.Value.(bool)
case d.propFanLevel.valid && p.SIID == d.propFanLevel.siid && p.PIID == d.propFanLevel.piid: state.FanLevel = toInt(p.Value)
case d.propOscillation.valid && p.SIID == d.propOscillation.siid && p.PIID == d.propOscillation.piid: state.Oscillation, _ = p.Value.(bool)
case d.propNaturalWind.valid && p.SIID == d.propNaturalWind.siid && p.PIID == d.propNaturalWind.piid: state.NaturalWind = toInt(p.Value) == 1
}
}
d.stateCache = state
return state, nil
}