Files
xiaomihome/bridge/bridge.go
T
4566704 a0ef854726 feat: 完善 AC"set 后回读"机制 — 目标聚合 + 防抖重试验证
**回读目标聚合(recordReadback)**
- 所有 set 方法(TurnOn/TurnOff/SetMode/SetTargetTemp/SetFanSpeed/
  SetSwing/SetTargetHumidity/SetAuxHeat/SetEco)成功后记录目标值 rbTarget
- 同属性目标覆盖、不同属性追加,连续 set 自动聚合

**防抖 + 重试验证(readbackLoop)**
- 静默窗口 readbackIdle=3s:等待最后一次 set 无新操作才开始验证
- 未命中目标时按 readbackRetryInterval=3s 重试,上限 readbackMaxRetries=5
- 整体超时 readbackTimeout=30s,防云端卡住导致回读永久挂起
- 命中后逐字段回调 OnPropsChanged 注册的 handler(与 MQTT 推送同语义)
- rbValueEqual 数值比较用 epsilon=0.01,bool/string 直接相等

**白名单配置透传**
- xiaomi.Config 新增 ReadbackModels 白名单(留空 = 全部不回读,默认)
- Client 新增 NeedReadback(model) 判定 + Logger() 访问器
- bridge.Pool.NewPool(readbackModels...) 透传;newBridge 传入 Client 配置

**其他**
- bridge 设备创建失败(Warn)与不支持跳过(Debug)分别记录日志
2026-08-23 23:33:32 +08:00

288 lines
8.8 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Package bridge 提供多用户小米设备管理门面。
//
// 外部只需 import 这一个包,无需直接依赖 xiaomi/devices/miot。
//
// 用法:
//
// pool := bridge.NewPool()
// pool.Add(ctx, id, uuid, auth, dataDir)
// b, _ := pool.Get(id)
// ac, _ := b.AC("did")
// ac.TurnOn(ctx)
// state := ac.GetProps()
package bridge
import (
"context"
"fmt"
"sync"
"xiaomihome/logger"
"xiaomihome/xiaomi"
"xiaomihome/xiaomi/devices"
)
// ── 类型别名:业务层不需要 import devices ──
type (
ACProps = devices.ACProps
LightProps = devices.LightProps
FanProps = devices.FanProps
HumidifierProps = devices.HumidifierProps
HeaterProps = devices.HeaterProps
VacuumProps = devices.VacuumProps
SwitchProps = devices.SwitchProps
SpeakerProps = devices.SpeakerProps
PlugProps = devices.PlugProps
)
// OccupancyProps 人体传感器状态(有人/无人)。
type OccupancyProps struct {
Occupied bool `json:"occupied"`
}
// ── Pool ──
// Pool 全局路由:id → Bridge。
type Pool struct {
mu sync.RWMutex
items map[int64]*Bridge
readbackModels []string // 需要 set 后回读的型号列表(透传到 xiaomi.Config)
}
// NewPool 创建 Pool。readbackModels 为需要"set 后回读"的设备型号列表(可选,默认空)。
func NewPool(readbackModels ...[]string) *Pool {
var models []string
if len(readbackModels) > 0 {
models = readbackModels[0]
}
return &Pool{items: make(map[int64]*Bridge), readbackModels: models}
}
// Get 按 id 查找。
func (p *Pool) Get(id int64) (*Bridge, bool) {
p.mu.RLock()
defer p.mu.RUnlock()
b, ok := p.items[id]
return b, ok
}
// Add 新建并注册一个 Bridge。
func (p *Pool) Add(ctx context.Context, id int64, uuid string, auth xiaomi.AuthInfo, dataDir string, homeIDs []int64) (*Bridge, error) {
p.mu.Lock()
defer p.mu.Unlock()
if _, exists := p.items[id]; exists {
return nil, fmt.Errorf("bridge: %d already exists", id)
}
b, err := newBridge(ctx, id, uuid, auth, dataDir, homeIDs, p.readbackModels)
if err != nil {
return nil, err
}
p.items[id] = b
return b, nil
}
// Remove 移除并关闭。
func (p *Pool) Remove(id int64) error {
p.mu.Lock()
b, ok := p.items[id]
if ok {
delete(p.items, id)
}
p.mu.Unlock()
if !ok {
return fmt.Errorf("bridge: %d not found", id)
}
return b.Close()
}
// Close 关闭所有。
func (p *Pool) Close() {
p.mu.Lock()
defer p.mu.Unlock()
for id, b := range p.items {
b.Close()
delete(p.items, id)
}
}
// ── Bridge ──
// Bridge 封装单个用户的 Client + 类型化设备控制器 + 状态缓存。
type Bridge struct {
id int64
client *xiaomi.Client
lgr logger.Logger
mu sync.RWMutex
dv map[string]interface{} // DID → devices.Xxx interface
onPropsChanged func(id int64, did, deviceType, changed string, props interface{})
onOnlineChanged func(id int64, did string, online bool)
}
func newBridge(ctx context.Context, id int64, uuid string, auth xiaomi.AuthInfo, dataDir string, homeIDs []int64, readbackModels []string) (*Bridge, error) {
idStr := fmt.Sprintf("%d", id)
client, err := xiaomi.NewClient(ctx, xiaomi.Config{
AuthInfo: auth,
DataDir: dataDir,
UUID: uuid,
UID: idStr,
HomeIDs: homeIDs,
ReadbackModels: readbackModels,
})
if err != nil {
return nil, fmt.Errorf("bridge: %w", err)
}
b := &Bridge{
id: id,
client: client,
lgr: logger.Default(),
dv: make(map[string]interface{}),
}
devList, err := client.GetDevices(ctx)
if err != nil {
client.Close()
return nil, fmt.Errorf("bridge: GetDevices: %w", err)
}
for _, info := range devList {
d, createErr := devices.Create(client, info)
if createErr != nil || d == nil {
if createErr != nil {
b.lgr.Warnf("[bridge] %d: create device %s (%s) failed: %v", id, info.DID, info.Model, createErr)
} else {
b.lgr.Debugf("[bridge] %d: device %s (%s) unsupported, skipped", id, info.DID, info.Model)
}
continue
}
b.dv[info.DID] = d
}
b.lgr.Infof("[bridge] %d: %d devices", id, len(b.dv))
return b, nil
}
// SetLogger 设置日志记录器。
func (b *Bridge) SetLogger(l logger.Logger) { b.lgr = l }
func (b *Bridge) Close() error { return b.client.Close() }
// Devices 返回设备列表(DID → 类型名)。
func (b *Bridge) Devices() map[string]string {
b.mu.RLock()
defer b.mu.RUnlock()
out := make(map[string]string, len(b.dv))
for did, d := range b.dv {
out[did] = deviceTypeName(d)
}
return out
}
func deviceTypeName(d interface{}) string {
switch d.(type) {
case devices.AirConditioner: return devices.TypeAirConditioner
case devices.Light: return devices.TypeLight
case devices.Switch: return devices.TypeSwitch
case devices.Fan: return devices.TypeFan
case devices.Cover: return devices.TypeCover
case devices.Humidifier: return devices.TypeHumidifier
case devices.Vacuum: return devices.TypeVacuum
case devices.WaterHeater: return devices.TypeWaterHeater
case devices.TempHumiditySensor: return devices.TypeTempHumidSensor
case devices.OccupancySensor: return devices.TypeOccupancySensor
case devices.Speaker: return devices.TypeSpeaker
case devices.Gateway: return devices.TypeGateway
case devices.Plug: return devices.TypePlug
default: return "unknown"
}
}
// ── 类型化设备访问器 ──
func (b *Bridge) AC(did string) (devices.AirConditioner, error) {
d, ok := b.dv[did]
if !ok { return nil, fmt.Errorf("bridge: device %s not found", did) }
ac, ok := d.(devices.AirConditioner)
if !ok { return nil, fmt.Errorf("bridge: device %s is not an AirConditioner (got %T)", did, d) }
return ac, nil
}
func (b *Bridge) Light(did string) (devices.Light, error) {
d, ok := b.dv[did]
if !ok { return nil, fmt.Errorf("bridge: device %s not found", did) }
lt, ok := d.(devices.Light)
if !ok { return nil, fmt.Errorf("bridge: device %s is not a Light", did) }
return lt, nil
}
func (b *Bridge) Switch(did string) (devices.Switch, error) {
d, ok := b.dv[did]
if !ok { return nil, fmt.Errorf("bridge: device %s not found", did) }
sw, ok := d.(devices.Switch)
if !ok { return nil, fmt.Errorf("bridge: device %s is not a Switch", did) }
return sw, nil
}
func (b *Bridge) Fan(did string) (devices.Fan, error) {
d, ok := b.dv[did]
if !ok { return nil, fmt.Errorf("bridge: device %s not found", did) }
f, ok := d.(devices.Fan)
if !ok { return nil, fmt.Errorf("bridge: device %s is not a Fan", did) }
return f, nil
}
func (b *Bridge) Cover(did string) (devices.Cover, error) {
d, ok := b.dv[did]
if !ok { return nil, fmt.Errorf("bridge: device %s not found", did) }
cv, ok := d.(devices.Cover)
if !ok { return nil, fmt.Errorf("bridge: device %s is not a Cover", did) }
return cv, nil
}
func (b *Bridge) Humidifier(did string) (devices.Humidifier, error) {
d, ok := b.dv[did]
if !ok { return nil, fmt.Errorf("bridge: device %s not found", did) }
h, ok := d.(devices.Humidifier)
if !ok { return nil, fmt.Errorf("bridge: device %s is not a Humidifier", did) }
return h, nil
}
func (b *Bridge) Vacuum(did string) (devices.Vacuum, error) {
d, ok := b.dv[did]
if !ok { return nil, fmt.Errorf("bridge: device %s not found", did) }
v, ok := d.(devices.Vacuum)
if !ok { return nil, fmt.Errorf("bridge: device %s is not a Vacuum", did) }
return v, nil
}
func (b *Bridge) WaterHeater(did string) (devices.WaterHeater, error) {
d, ok := b.dv[did]
if !ok { return nil, fmt.Errorf("bridge: device %s not found", did) }
wh, ok := d.(devices.WaterHeater)
if !ok { return nil, fmt.Errorf("bridge: device %s is not a WaterHeater", did) }
return wh, nil
}
func (b *Bridge) TempHumidity(did string) (devices.TempHumiditySensor, error) {
d, ok := b.dv[did]
if !ok { return nil, fmt.Errorf("bridge: device %s not found", did) }
s, ok := d.(devices.TempHumiditySensor)
if !ok { return nil, fmt.Errorf("bridge: device %s is not a TempHumiditySensor", did) }
return s, nil
}
func (b *Bridge) Occupancy(did string) (devices.OccupancySensor, error) {
d, ok := b.dv[did]
if !ok { return nil, fmt.Errorf("bridge: device %s not found", did) }
s, ok := d.(devices.OccupancySensor)
if !ok { return nil, fmt.Errorf("bridge: device %s is not an OccupancySensor", did) }
return s, nil
}
func (b *Bridge) Plug(did string) (devices.Plug, error) {
d, ok := b.dv[did]
if !ok { return nil, fmt.Errorf("bridge: device %s not found", did) }
pg, ok := d.(devices.Plug)
if !ok { return nil, fmt.Errorf("bridge: device %s is not a Plug", did) }
return pg, nil
}
func (b *Bridge) Speaker(did string) (devices.Speaker, error) {
d, ok := b.dv[did]
if !ok { return nil, fmt.Errorf("bridge: device %s not found", did) }
sp, ok := d.(devices.Speaker)
if !ok { return nil, fmt.Errorf("bridge: device %s is not a Speaker", did) }
return sp, nil
}