在米家 App 删除设备后设备列表仍能获取到:删除检测只 delete(deviceListCloud), deviceListCache 仅置 online=false,而对外 DeviceList()/GetDevices() 读的正是 cache;缓存又会在停机时落盘到 .dict、启动时原样恢复,已删设备跨重启复活。 - pruneRemovedDevicesLocked:候选集改为「缓存 ∪ 云列表」,云端不存在且网关/ 局域网非在线的设备从四张表彻底删除(旧实现只遍历 deviceListCloud,启动时它 为空,停机期间被删的设备永远检测不到) - 护栏:云端返回空列表时跳过剔除,避免接口异常清库 - homeScope:只刷新部分家庭时不再误删其它家庭的云列表 - 子设备(xxx.s1)不参与判定,它被归并到父设备的 sub_devices - 命中剔除的设备同时清理 c.devices 实例与 MQTT 订阅路由 - bridge: pruneControllers 剔除已删设备的控制器,AC()/Switch() 不再返回实例 - 新增 7 个 miot 用例(含 httptest 打桩的端到端)+ 3 个 bridge 用例
301 lines
9.2 KiB
Go
301 lines
9.2 KiB
Go
// 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
|
||
}
|
||
|
||
// pruneControllers 从本地控制器集合中剔除「米家已不存在」的设备,返回被剔除的 DID。
|
||
// 没有这一步,AC()/Switch()/Fan() 等访问器会继续为已删设备返回控制器,控制必然失败。
|
||
func pruneControllers(dv map[string]interface{}, alive map[string]struct{}) []string {
|
||
var removed []string
|
||
for did := range dv {
|
||
if _, ok := alive[did]; !ok {
|
||
delete(dv, did)
|
||
removed = append(removed, did)
|
||
}
|
||
}
|
||
return removed
|
||
}
|
||
|
||
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
|
||
}
|