Files
xiaomihome/xiaomi/devices/switch.go
T
4566704 04333750ab feat: State→Props 命名统一 + 新增 Plug/Gateway 设备 + 分类系统重构
**命名统一(State → Props)**
- 所有设备类型:XxxState → XxxProps,GetState() → GetProps(),FetchState() → FetchProps()
- 回调接口:OnStateChanged → OnPropsChanged
- Bridge 层类型别名、示例代码、文档同步更名

**新增设备类型**
- KindPlug + Plug 驱动:智能插座/插头(TurnOn/TurnOff/IsOn/OnPropsChanged)
- KindGateway + Gateway 驱动:网关设备类型识别
- KindOccupancySensor 从 KindSensor 独立,添加 OccupancyProps

**设备分类系统改进**
- classifyByModel 改为按 key 长度降序匹配(防止 sensor 先于 sensor-occupy)
- 新增下划线变体识别(sensor_occupy、sensor_temp 等)
- outlet 重新归类为 Plug(非 Switch)
- 新增 Or() 组合过滤器

**连接桥增强**
- 设备创建成功后自动订阅属性变更
- 新增 nil 防护检查
- 支持 Plug/Speaker/Gateway/Occupancy 类型的 FetchProps 刷新
- 日志级别 Info → Debug

**AC 驱动增强**
- 新增 HasPower()、GetElectricPower()、GetPowerConsumption() 功率接口
- ACProps 新增 ElectricPower、PowerConsumption 字段
- valMapper:Desc 为空时 fallback 用 Name,key 统一小写

**MIoT 核心**
- 设备列表刷新改用 reflect.DeepEqual 全字段变更检测
- 移除 BLE/代理设备默认在线 hack
- RefreshDeviceAllProps 增加离线检查
- 新增 miot_errors.go:MIoT 错误码定义
- 全局日志级别 Info→Debug/Warn(降低噪音)

**示例与文档**
- 所有示例代码同步 API 变更
- 传感器示例改用 Or 组合过滤器
- ARCH_PLAN 等迁移文档同步更新
2026-08-05 17:54:08 +08:00

185 lines
4.7 KiB
Go

package devices
import (
"context"
"xiaomihome/xiaomi"
)
// Switch 开关统一控制接口。
// 单通道:TurnOn/TurnOff/Toggle/IsOn 操作唯一通道。
// 多通道:ChannelCount > 1 时,TurnOn 等操作通道 1,或使用带 channel 参数的方法。
type Switch interface {
Device
TurnOn(ctx context.Context) error
TurnOff(ctx context.Context) error
Toggle(ctx context.Context) error
IsOn(ctx context.Context) (bool, error)
// 多通道开关支持(单通道时自动回退到上述方法)
ChannelCount() int
TurnOnChannel(ctx context.Context, ch int) error
TurnOffChannel(ctx context.Context, ch int) error
ToggleChannel(ctx context.Context, ch int) error
IsChannelOn(ctx context.Context, ch int) (bool, error)
OnPropsChanged(handler func(ch int, on bool)) (string, error)
GetProps() SwitchProps
FetchProps(ctx context.Context) (SwitchProps, error)
}
// SwitchProps 多键开关完整状态。
// JSON: {"on":[true,false,true]},索引 0 = 通道 1
type SwitchProps struct {
On []bool `json:"on"`
}
// switchDevice is the concrete Switch implementation.
type switchDevice struct {
BaseDevice
channels []siidPiid // "on" property per channel (index 0 = channel 1)
propsCache SwitchProps
}
type siidPiid struct {
siid, piid int
valid bool
}
// NewSwitch creates a Switch control for the given device.
func NewSwitch(client *xiaomi.Client, info *xiaomi.DeviceInfo) (Switch, error) {
d := &switchDevice{
BaseDevice: NewBaseDevice(client, info),
}
miotDev := d.getMIoTDevice()
if miotDev == nil || miotDev.SpecInstance() == nil {
return nil, xiaomi.ErrNoSPEC
}
spec := miotDev.SpecInstance()
var channels []siidPiid
for _, svc := range spec.Services {
for _, prop := range svc.Properties {
if propertyShortName(prop.Type) == "on" {
channels = append(channels, siidPiid{prop.SIID, prop.PIID, true})
}
}
}
if len(channels) == 0 {
return nil, xiaomi.ErrNotSupported
}
d.channels = channels
d.propsCache.On = make([]bool, len(channels))
return d, nil
}
func (d *switchDevice) ChannelCount() int {
return len(d.channels)
}
func (d *switchDevice) TurnOn(ctx context.Context) error { return d.TurnOnChannel(ctx, 1) }
func (d *switchDevice) TurnOff(ctx context.Context) error { return d.TurnOffChannel(ctx, 1) }
func (d *switchDevice) Toggle(ctx context.Context) error { return d.ToggleChannel(ctx, 1) }
func (d *switchDevice) IsOn(ctx context.Context) (bool, error) {
return d.IsChannelOn(ctx, 1)
}
func (d *switchDevice) TurnOnChannel(ctx context.Context, ch int) error {
p, ok := d.channel(ch)
if !ok {
return xiaomi.ErrNotSupported
}
return d.SetProp(ctx, p.siid, p.piid, true)
}
func (d *switchDevice) TurnOffChannel(ctx context.Context, ch int) error {
p, ok := d.channel(ch)
if !ok {
return xiaomi.ErrNotSupported
}
return d.SetProp(ctx, p.siid, p.piid, false)
}
func (d *switchDevice) ToggleChannel(ctx context.Context, ch int) error {
on, err := d.IsChannelOn(ctx, ch)
if err != nil {
return err
}
if on {
return d.TurnOffChannel(ctx, ch)
}
return d.TurnOnChannel(ctx, ch)
}
func (d *switchDevice) IsChannelOn(ctx context.Context, ch int) (bool, error) {
p, ok := d.channel(ch)
if !ok {
return false, xiaomi.ErrNotSupported
}
prop, err := d.GetProp(ctx, p.siid, p.piid)
if err != nil {
return false, err
}
if b, ok := prop.Value.(bool); ok {
return b, nil
}
return false, nil
}
func (d *switchDevice) OnPropsChanged(handler func(ch int, on bool)) (string, error) {
if len(d.channels) == 0 {
return "", xiaomi.ErrNotSupported
}
type chField struct{ siid, piid int }
lookup := make(map[chField]int)
for i, c := range d.channels {
lookup[chField{c.siid, c.piid}] = i
}
return d.SubProp(0, 0, func(did string, prop *xiaomi.PropertyValue) {
for k, idx := range lookup {
if k.siid == prop.SIID && k.piid == prop.PIID {
if b, ok := prop.Value.(bool); ok {
d.propsCache.On[idx] = b
handler(idx+1, b)
}
return
}
}
})
}
func (d *switchDevice) channel(ch int) (siidPiid, bool) {
i := ch - 1
if i < 0 || i >= len(d.channels) {
return siidPiid{}, false
}
return d.channels[i], true
}
func (d *switchDevice) GetProps() SwitchProps { return d.propsCache }
func (d *switchDevice) FetchProps(ctx context.Context) (SwitchProps, error) {
keys := make([]xiaomi.PropKey, len(d.channels))
for i, c := range d.channels {
keys[i] = xiaomi.PropKey{DID: d.info.DID, SIID: c.siid, PIID: c.piid}
}
props, err := d.client.GetProps(ctx, keys)
if err != nil {
return d.propsCache, err
}
states := make([]bool, len(d.channels))
for i, c := range d.channels {
for _, p := range props {
if p.SIID == c.siid && p.PIID == c.piid {
states[i], _ = p.Value.(bool)
break
}
}
}
d.propsCache.On = states
return d.propsCache, nil
}