chore: 提交高层 xiaomi 封装库及示例

- 新增 xiaomi/examples/ 下 8 个示例程序(用户/家庭查询、设备列表、开关/灯光/空调控制、属性订阅、高级过滤分类、SPEC 解析)
- miot_client_sub.go: 重构 SubProp/SubEvent,使用 buildPropTopic/buildEventTopic 支持通配符订阅(siid/piid=0 → +),并修复锁顺序问题(将 RequestRefreshProp 移到 Lock 外)
- spec_parser.go: 新增 downloadSpecFile 方法,本地 SPEC 文件缺失时自动从 miot-spec.org 下载
- ARCH_PLAN.md: 架构设计从 Proposed 更新为 Accepted(v1.0→v1.1),补充设备分类/工厂/SPEC 映射等模块设计
- xiaomi/: 新增 miot 上层强类型封装模块,包含 Client 主入口、用户/家庭/设备 API、属性读写、动作调用、订阅通知,以及 devices/ 设备控制抽象(Switch/Light/AirConditioner/Fan/Cover/Humidifier/Vacuum/WaterHeater/Thermostat)和 specs/ SPEC 查询辅助
- 更多 xiaomi 示例(风扇/窗帘/传感器控制)
This commit is contained in:
2026-06-29 08:51:21 +08:00
parent 35efed7d15
commit a3d94c4b9f
93 changed files with 7582 additions and 157 deletions
+140
View File
@@ -0,0 +1,140 @@
package devices
import (
"context"
"xiaomihome/xiaomi"
)
// Thermostat 温控器/电热毯统一控制接口
type Thermostat interface {
Device
TurnOn(ctx context.Context) error
TurnOff(ctx context.Context) error
IsOn(ctx context.Context) (bool, error)
SetTargetTemp(ctx context.Context, celsius float64) error
GetTargetTemp(ctx context.Context) (float64, error)
GetCurrentTemp(ctx context.Context) (float64, error)
SetHeating(ctx context.Context, on bool) error
IsHeating(ctx context.Context) (bool, error)
}
type thermostatDevice struct {
BaseDevice
propOn siidPiid // "on"
propTargetTemp siidPiid // "target-temperature"
propCurrentTemp siidPiid // "temperature"
propHeating siidPiid // "heating" or "mode"
}
// NewThermostat creates a Thermostat control for the given device.
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
}
siid, piid, ok := resolver.find("on")
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("heating"); ok {
d.propHeating = siidPiid{s, p, true}
} else 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
}
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
}
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
}
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 *thermostatDevice) SetTargetTemp(ctx context.Context, celsius float64) error {
if !d.propTargetTemp.valid {
return xiaomi.ErrNotSupported
}
return d.SetProp(ctx, d.propTargetTemp.siid, d.propTargetTemp.piid, celsius)
}
func (d *thermostatDevice) GetTargetTemp(ctx context.Context) (float64, error) {
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
}
return toFloat(prop.Value), nil
}
func (d *thermostatDevice) GetCurrentTemp(ctx context.Context) (float64, error) {
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
}
return toFloat(prop.Value), nil
}
func (d *thermostatDevice) SetHeating(ctx context.Context, on bool) error {
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
}
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
}
return false, nil
}