Files
xiaomihome/bridge/bridge.go
T
4566704 a595330144 fix(miot): 米家已删除设备从缓存、控制器与订阅中彻底剔除
在米家 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 用例
2026-09-12 19:29:46 +08:00

301 lines
9.2 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
}
// 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
}