// Package devices provides strongly-typed device control abstractions for // Xiaomi smart home devices. Each device type defines an interface that hides // siid/piid details behind semantic methods like SetTargetTemp() and SetMode(). // // The package uses SPEC property resolution to automatically discover the // correct siid/piid mappings for each device model, enabling cross-model // compatibility without hardcoded property IDs. package devices import ( "context" "strings" "xiaomihome/miot" "xiaomihome/xiaomi" ) // Device is the common interface shared by all device control types. // All device kinds (Switch, Light, AirConditioner, etc.) embed this interface. type Device interface { // DID returns the device's unique identifier. DID() string // Name returns the device's display name. Name() string // Model returns the device's model identifier. Model() string // Online returns whether the device is currently online. Online() bool // Info returns the full device information. Info() *xiaomi.DeviceInfo } // BaseDevice provides common functionality shared by all device types. // Each device type embeds BaseDevice and extends it with type-specific methods. // BaseDevice implements the Device interface. type BaseDevice struct { client *xiaomi.Client info *xiaomi.DeviceInfo miotDev *miot.MIoTDevice resolver *specResolver // cached SPEC resolver (created on demand) } // NewBaseDevice creates a new BaseDevice instance. func NewBaseDevice(client *xiaomi.Client, info *xiaomi.DeviceInfo) BaseDevice { return BaseDevice{ client: client, info: info, } } // DID returns the device's unique identifier. func (d *BaseDevice) DID() string { return d.info.DID } // Name returns the device's display name. func (d *BaseDevice) Name() string { d.refreshInfo() return d.info.Name } // Model returns the device's model identifier. func (d *BaseDevice) Model() string { d.refreshInfo() return d.info.Model } // Online returns whether the device is currently online. func (d *BaseDevice) Online() bool { d.refreshInfo() return d.info.Online } // refreshInfo 从客户端缓存拉取最新设备信息,有变化时原地更新。 func (d *BaseDevice) refreshInfo() { info, err := d.client.GetDevice(context.Background(), d.info.DID) if err != nil || info == nil { return } d.info = info } // Info returns the full device information. func (d *BaseDevice) Info() *xiaomi.DeviceInfo { return d.info } // GetProp reads a single property value from the device. func (d *BaseDevice) GetProp(ctx context.Context, siid, piid int) (*xiaomi.PropertyValue, error) { return d.client.GetProp(ctx, d.info.DID, siid, piid) } // SetProp writes a single property value to the device. func (d *BaseDevice) SetProp(ctx context.Context, siid, piid int, value interface{}) error { return d.client.SetProp(ctx, d.info.DID, siid, piid, value) } // Ping 通过实际读属性验证设备是否在线,不走缓存。 func (d *BaseDevice) Ping(ctx context.Context, siid, piid int) bool { _, err := d.GetProp(ctx, siid, piid) return err == nil } // Action invokes a device action. func (d *BaseDevice) Action(ctx context.Context, siid, aiid int, params []interface{}) (*xiaomi.ActionResult, error) { return d.client.Action(ctx, d.info.DID, siid, aiid, params) } // SubProp subscribes to property change notifications. func (d *BaseDevice) SubProp(siid, piid int, handler xiaomi.PropHandler) (string, error) { return d.client.SubProp(d.info.DID, siid, piid, handler) } // SubEvent subscribes to event notifications. func (d *BaseDevice) SubEvent(siid, eiid int, handler xiaomi.EventHandler) (string, error) { return d.client.SubEvent(d.info.DID, siid, eiid, handler) } // SubDeviceState subscribes to online/offline state changes. func (d *BaseDevice) SubDeviceState(handler xiaomi.DeviceStateHandler) error { return d.client.SubDeviceState(d.info.DID, handler) } // getMIoTDevice returns the underlying MIoTDevice (lazy-loaded). func (d *BaseDevice) getMIoTDevice() *miot.MIoTDevice { if d.miotDev == nil { d.miotDev = d.client.GetMIoTDevice(d.info.DID) } return d.miotDev } // getSpecResolver returns a SPEC property resolver (lazy-loaded). func (d *BaseDevice) getSpecResolver() *specResolver { if d.resolver == nil { dev := d.getMIoTDevice() if dev != nil { d.resolver = newSpecResolver(dev.SpecInstance()) } } return d.resolver } // specResolver is a thin wrapper to avoid importing specs package in every device file. type specResolver struct { byType map[string]*miot.MIoTSpecProperty // property short name → first match services []*miot.MIoTSpecService // 保留服务列表(支持按服务查找) } func newSpecResolver(spec *miot.MIoTSpecInstance) *specResolver { if spec == nil { return nil } r := &specResolver{ byType: make(map[string]*miot.MIoTSpecProperty), services: spec.Services, } for _, svc := range spec.Services { for i := range svc.Properties { prop := svc.Properties[i] name := propertyShortName(prop.Type) if name != "" { if _, exists := r.byType[name]; !exists { r.byType[name] = prop } } } } return r } // propertyShortName extracts the short name from a SPEC property URN. // E.g., "urn:miot-spec-v2:property:on:00000006:..." → "on". func propertyShortName(urn string) string { parts := strings.Split(urn, ":") if len(parts) >= 4 && parts[2] == "property" { return parts[3] } return urn } // serviceShortName extracts the short name from a SPEC service URN. // E.g., "urn:miot-spec-v2:service:thermostat:0000784A:..." → "thermostat". // 注意:不能用 propertyShortName 解析服务 URN(其只认 parts[2]=="property")。 func serviceShortName(urn string) string { parts := strings.Split(urn, ":") if len(parts) >= 4 && parts[2] == "service" { return parts[3] } return urn } func (r *specResolver) find(propType string) (siid, piid int, found bool) { if r == nil { return 0, 0, false } if prop, ok := r.byType[propType]; ok { return prop.SIID, prop.PIID, true } return 0, 0, false } // findInService 在指定服务(短名,如 "thermostat")内查找属性。 // 用于多服务同名属性冲突时按服务精确定位(如三合一设备空调实体在 thermostat 服务)。 func (r *specResolver) findInService(svcName, propType string) (siid, piid int, found bool) { if r == nil { return 0, 0, false } for _, svc := range r.services { if svc == nil || serviceShortName(svc.Type) != svcName { continue } for _, prop := range svc.Properties { if prop != nil && propertyShortName(prop.Type) == propType { return svc.IID, prop.PIID, true } } } return 0, 0, false } // hasService 判断设备是否包含指定服务(短名)。 func (r *specResolver) hasService(svcName string) bool { if r == nil { return false } for _, svc := range r.services { if svc != nil && serviceShortName(svc.Type) == svcName { return true } } return false } // prop returns the full SPEC property for the given type name. func (r *specResolver) prop(propType string) *miot.MIoTSpecProperty { if r == nil { return nil } return r.byType[propType] } // format returns the SPEC Format for a property type ("bool", "uint8", "float", "string"). func (r *specResolver) format(propType string) string { if r == nil { return "" } if prop, ok := r.byType[propType]; ok { return prop.Format } return "" }