// 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 { return d.info.Name } // Model returns the device's model identifier. func (d *BaseDevice) Model() string { return d.info.Model } // Online returns whether the device is currently online. func (d *BaseDevice) Online() bool { return d.info.Online } // 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) } // 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 } func newSpecResolver(spec *miot.MIoTSpecInstance) *specResolver { if spec == nil { return nil } r := &specResolver{ byType: make(map[string]*miot.MIoTSpecProperty), } 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 } 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 }