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
+119
View File
@@ -0,0 +1,119 @@
package xiaomi
import (
"context"
"fmt"
"sync"
"xiaomihome/miot"
)
// Client 高级封装客户端(包装 miot.MIoTClient + miot.MIoTHttpClient)
type Client struct {
cfg Config
inner *miot.MIoTClient
http *miot.MIoTHttpClient
mipsCloud *miot.MipsCloudClient
specParser *miot.MIoTSpecParser
deviceMu sync.RWMutex
devices map[string]*miot.MIoTDevice
propSubs map[string]string
eventSubs map[string]string
mu sync.RWMutex
}
// NewClient 创建并初始化客户端(零 miot import,一个 Config 搞定)。
func NewClient(ctx context.Context, cfg Config) (*Client, error) {
entryData := map[string]interface{}{
"access_token": cfg.AccessToken,
}
miotClient := miot.NewMIoTClient("xiaomi_sdk", entryData, cfg.UID, "cn", miot.CtrlModeAuto)
miotClient.SetStorage(miot.NewMIoTStorage("./data"))
miotClient.SetSpecParser(miot.NewMIoTSpecParser("en", miot.NewMIoTStorage("./data"), "./data/specs"))
httpClient, err := miot.NewMIoTHttpClient("cn", miot.OAUTH2_CLIENT_ID, cfg.AccessToken)
if err != nil {
return nil, fmt.Errorf("xiaomi: http: %w", err)
}
miotClient.SetHTTPClient(httpClient)
// MIPS client ID 使用 config 中的 UUID,空则自动生成
mipsUUID := cfg.UUID
if mipsUUID == "" {
mipsUUID = generateUUID()
}
mipsCloud := miot.NewMipsCloudClient(
"ssl://cn-ha.mqtt.io.mi.com:8883",
"ha."+mipsUUID,
miot.OAUTH2_CLIENT_ID,
cfg.AccessToken,
)
miotClient.SetMipsCloud(mipsCloud)
if err := miotClient.Init(); err != nil {
return nil, fmt.Errorf("xiaomi: init: %w", err)
}
// 初始刷新设备列表(对齐 full_client 示例)
_ = miotClient.RefreshDevices()
// 连接 MIPS(对齐 full_client 示例 line 83)
if err := mipsCloud.Connect(); err != nil {
return nil, fmt.Errorf("xiaomi: mips connect: %w", err)
}
return &Client{
cfg: cfg,
inner: miotClient,
http: httpClient,
mipsCloud: mipsCloud,
specParser: miot.NewMIoTSpecParser("en", miot.NewMIoTStorage("./data"), "./data/specs"),
devices: make(map[string]*miot.MIoTDevice),
propSubs: make(map[string]string),
eventSubs: make(map[string]string),
}, nil
}
// Close 关闭客户端。
func (c *Client) Close() error {
if c.mipsCloud != nil {
c.mipsCloud.Disconnect()
}
if c.inner != nil {
return c.inner.Deinit()
}
return nil
}
func (c *Client) getOrCreateDevice(did string, info map[string]interface{}) *miot.MIoTDevice {
c.deviceMu.Lock()
defer c.deviceMu.Unlock()
if dev, ok := c.devices[did]; ok {
return dev
}
// 从 device info 获取 URN 和 model 用于 SPEC 加载
urn, _ := info["urn"].(string)
model, _ := info["model"].(string)
var spec *miot.MIoTSpecInstance
if c.specParser != nil && urn != "" {
s, err := c.specParser.Parse(urn, model)
if err == nil {
spec = s
} else {
fmt.Printf("[xiaomi] spec parse failed for %s (%s): %v\n", did, urn, err)
}
}
dev := miot.NewMIoTDevice(c.inner, info, spec)
c.devices[did] = dev
return dev
}
func (c *Client) getDevice(did string) *miot.MIoTDevice {
c.deviceMu.RLock()
defer c.deviceMu.RUnlock()
return c.devices[did]
}