package devices import ( "context" "xiaomihome/xiaomi" ) // Light 灯光统一控制接口 type Light interface { Device TurnOn(ctx context.Context) error TurnOff(ctx context.Context) error Toggle(ctx context.Context) error // 能力查询 HasBrightness() bool HasColorTemp() bool HasColor() bool // 亮度 (1-100) SetBrightness(ctx context.Context, pct int) error GetBrightness(ctx context.Context) (int, error) // 色温 (2700-6500K) SetColorTemp(ctx context.Context, kelvin int) error GetColorTemp(ctx context.Context) (int, error) // 颜色 (0-255) SetColor(ctx context.Context, r, g, b int) error GetColor(ctx context.Context) (r, g, b int, err error) OnStateChanged(handler func(state LightState, changed string)) (string, error) GetState() LightState FetchState(ctx context.Context) (LightState, error) } // LightState 灯光完整状态(字段名对齐 SPEC type) type LightState struct { On bool `json:"on"` // SPEC: "on" Brightness int `json:"brightness"` // SPEC: "brightness" ColorTemperature int `json:"colorTemperature"` // SPEC: "color-temperature" ColorRGB [3]int `json:"colorRgb"` // SPEC: "color" } type lightDevice struct { BaseDevice propOn siidPiid // "on" propBrightness siidPiid // "brightness" propColorTemp siidPiid // "color-temperature" propColor siidPiid // "color" stateCache LightState // MQTT 推送维护的本地缓存 } // NewLight creates a Light control for the given device. func NewLight(client *xiaomi.Client, info *xiaomi.DeviceInfo) (Light, error) { d := &lightDevice{ BaseDevice: NewBaseDevice(client, info), } resolver := d.getSpecResolver() if resolver == nil { return nil, xiaomi.ErrNoSPEC } // "on" is required siid, piid, ok := resolver.find("on") if !ok { return nil, xiaomi.ErrNotSupported } d.propOn = siidPiid{siid, piid, true} // Optional properties if s, p, ok := resolver.find("brightness"); ok { d.propBrightness = siidPiid{s, p, true} } if s, p, ok := resolver.find("color-temperature"); ok { d.propColorTemp = siidPiid{s, p, true} } if s, p, ok := resolver.find("color"); ok { d.propColor = siidPiid{s, p, true} } return d, nil } func (d *lightDevice) TurnOn(ctx context.Context) error { if !d.propOn.valid { return xiaomi.ErrNotSupported } return d.SetProp(ctx, d.propOn.siid, d.propOn.piid, true) } func (d *lightDevice) TurnOff(ctx context.Context) error { if !d.propOn.valid { return xiaomi.ErrNotSupported } return d.SetProp(ctx, d.propOn.siid, d.propOn.piid, false) } func (d *lightDevice) Toggle(ctx context.Context) error { on, err := d.isOn(ctx) if err != nil { return err } if on { return d.TurnOff(ctx) } return d.TurnOn(ctx) } func (d *lightDevice) isOn(ctx context.Context) (bool, error) { prop, err := d.GetProp(ctx, d.propOn.siid, d.propOn.piid) if err != nil { return false, err } if b, ok := prop.Value.(bool); ok { return b, nil } return false, nil } func (d *lightDevice) HasBrightness() bool { return d.propBrightness.valid } func (d *lightDevice) HasColorTemp() bool { return d.propColorTemp.valid } func (d *lightDevice) HasColor() bool { return d.propColor.valid } func (d *lightDevice) SetBrightness(ctx context.Context, pct int) error { if !d.propBrightness.valid { return xiaomi.ErrNotSupported } return d.SetProp(ctx, d.propBrightness.siid, d.propBrightness.piid, pct) } func (d *lightDevice) GetBrightness(ctx context.Context) (int, error) { if !d.propBrightness.valid { return 0, xiaomi.ErrNotSupported } prop, err := d.GetProp(ctx, d.propBrightness.siid, d.propBrightness.piid) if err != nil { return 0, err } return toInt(prop.Value), nil } func (d *lightDevice) SetColorTemp(ctx context.Context, kelvin int) error { if !d.propColorTemp.valid { return xiaomi.ErrNotSupported } return d.SetProp(ctx, d.propColorTemp.siid, d.propColorTemp.piid, kelvin) } func (d *lightDevice) GetColorTemp(ctx context.Context) (int, error) { if !d.propColorTemp.valid { return 0, xiaomi.ErrNotSupported } prop, err := d.GetProp(ctx, d.propColorTemp.siid, d.propColorTemp.piid) if err != nil { return 0, err } return toInt(prop.Value), nil } func (d *lightDevice) SetColor(ctx context.Context, r, g, b int) error { if !d.propColor.valid { return xiaomi.ErrNotSupported } colorVal := (r << 16) | (g << 8) | b return d.SetProp(ctx, d.propColor.siid, d.propColor.piid, colorVal) } func (d *lightDevice) GetColor(ctx context.Context) (r, g, b int, err error) { if !d.propColor.valid { return 0, 0, 0, xiaomi.ErrNotSupported } prop, err := d.GetProp(ctx, d.propColor.siid, d.propColor.piid) if err != nil { return 0, 0, 0, err } val := toInt(prop.Value) r = (val >> 16) & 0xFF g = (val >> 8) & 0xFF b = val & 0xFF return r, g, b, nil } func (d *lightDevice) OnStateChanged(handler func(state LightState, changed string)) (string, error) { type field struct { siid, piid int name string fill func(*LightState, interface{}) } var fields []field if d.propOn.valid { fields = append(fields, field{d.propOn.siid, d.propOn.piid, "on", func(s *LightState, v interface{}) { s.On, _ = v.(bool) }}) } if d.propBrightness.valid { fields = append(fields, field{d.propBrightness.siid, d.propBrightness.piid, "brightness", func(s *LightState, v interface{}) { s.Brightness = toInt(v) }}) } if d.propColorTemp.valid { fields = append(fields, field{d.propColorTemp.siid, d.propColorTemp.piid, "color-temperature", func(s *LightState, v interface{}) { s.ColorTemperature = toInt(v) }}) } if d.propColor.valid { fields = append(fields, field{d.propColor.siid, d.propColor.piid, "color", func(s *LightState, v interface{}) { if v, ok := v.(float64); ok { c := int(v) s.ColorRGB = [3]int{(c >> 16) & 0xFF, (c >> 8) & 0xFF, c & 0xFF} } }}) } if len(fields) == 0 { return "", xiaomi.ErrNotSupported } return d.SubProp(0, 0, func(did string, prop *xiaomi.PropertyValue) { for _, f := range fields { if f.siid == prop.SIID && f.piid == prop.PIID { f.fill(&d.stateCache, prop.Value) handler(d.stateCache, f.name) return } } }) } // toInt converts an interface{} value to int. func toInt(v interface{}) int { switch n := v.(type) { case float64: return int(n) case int: return n case int64: return int(n) case uint8: return int(n) case uint16: return int(n) case uint32: return int(n) } return 0 } func (d *lightDevice) GetState() LightState { return d.stateCache } func (d *lightDevice) FetchState(ctx context.Context) (LightState, error) { keys := []xiaomi.PropKey{} if d.propOn.valid { keys = append(keys, xiaomi.PropKey{DID: d.info.DID, SIID: d.propOn.siid, PIID: d.propOn.piid}) } if d.propBrightness.valid { keys = append(keys, xiaomi.PropKey{DID: d.info.DID, SIID: d.propBrightness.siid, PIID: d.propBrightness.piid}) } if d.propColorTemp.valid { keys = append(keys, xiaomi.PropKey{DID: d.info.DID, SIID: d.propColorTemp.siid, PIID: d.propColorTemp.piid}) } if d.propColor.valid { keys = append(keys, xiaomi.PropKey{DID: d.info.DID, SIID: d.propColor.siid, PIID: d.propColor.piid}) } props, err := d.client.GetProps(ctx, keys) if err != nil { return d.stateCache, err } state := LightState{} for _, p := range props { switch { case d.propOn.valid && p.SIID == d.propOn.siid && p.PIID == d.propOn.piid: state.On, _ = p.Value.(bool) case d.propBrightness.valid && p.SIID == d.propBrightness.siid && p.PIID == d.propBrightness.piid: state.Brightness = toInt(p.Value) case d.propColorTemp.valid && p.SIID == d.propColorTemp.siid && p.PIID == d.propColorTemp.piid: state.ColorTemperature = toInt(p.Value) case d.propColor.valid && p.SIID == d.propColor.siid && p.PIID == d.propColor.piid: if v, ok := p.Value.(float64); ok { c := int(v); state.ColorRGB = [3]int{(c>>16)&0xFF, (c>>8)&0xFF, c&0xFF} } } } d.stateCache = state return state, nil }