package devices import ( "context" "strings" "git.zeroonesoft.cn/golib/xiaomihome/miot" "git.zeroonesoft.cn/golib/xiaomihome/xiaomi" ) // Speaker 音箱通用控制接口 type Speaker interface { Device TurnOn(ctx context.Context) error TurnOff(ctx context.Context) error Toggle(ctx context.Context) error // 能力查询 HasVolume() bool HasMute() bool HasPlayControl() bool // 音量 (0-100) SetVolume(ctx context.Context, pct int) error GetVolume(ctx context.Context) (int, error) // 静音 SetMute(ctx context.Context, mute bool) error GetMute(ctx context.Context) (bool, error) // 播放控制 (0=停止, 1=播放, 2=暂停) SetPlayState(ctx context.Context, state int) error GetPlayState(ctx context.Context) (int, error) // ── 智能音箱 (intelligent-speaker) ── // 睡眠模式 HasSleepMode() bool SetSleepMode(ctx context.Context, sleep bool) error GetSleepMode(ctx context.Context) (bool, error) // 音频 ID (只读/通知) GetAudioID(ctx context.Context) (string, error) // 动作 WakeUp(ctx context.Context) error PlayRadio(ctx context.Context) error PlayText(ctx context.Context, text string) error ExecuteTextDirective(ctx context.Context, text string, silent bool) error PlayMusic(ctx context.Context) error OnPropsChanged(handler func(props SpeakerProps, changed string)) (string, error) GetProps() SpeakerProps FetchProps(ctx context.Context) (SpeakerProps, error) } // SpeakerProps 音箱完整状态(字段名对齐 SPEC type) type SpeakerProps struct { On bool `json:"on"` // SPEC: "on" Volume int `json:"volume"` // SPEC: "volume" Mute bool `json:"mute"` // SPEC: "mute" PlayState int `json:"playState"` // SPEC: "tds-in" or "playing-state" (0=stop,1=play,2=pause) SleepMode bool `json:"sleepMode"` // SPEC: "sleep-mode" (intelligent-speaker) AudioID string `json:"audioId"` // SPEC: "audio-id" (intelligent-speaker, read-only) } // isAction 智能音箱动作 siid/aiid 对 type isAction struct{ siid, aiid int } type speakerDevice struct { BaseDevice propOn siidPiid // "on" propVolume siidPiid // "volume" propMute siidPiid // "mute" propPlayState siidPiid // "playing-state" or "tds-in" propSleepMode siidPiid // "sleep-mode" (intelligent-speaker) propAudioID siidPiid // "audio-id" (intelligent-speaker) // intelligent-speaker 服务 siid isSiid int // 动作 actWakeUp isAction actPlayRadio isAction actPlayText isAction actExecuteTextDirective isAction actPlayMusic isAction propsCache SpeakerProps } // NewSpeaker creates a Speaker control for the given device. func NewSpeaker(client *xiaomi.Client, info *xiaomi.DeviceInfo) (Speaker, error) { d := &speakerDevice{ BaseDevice: NewBaseDevice(client, info), } dev := d.getMIoTDevice() if dev == nil { return nil, xiaomi.ErrNoSPEC } spec := dev.SpecInstance() if spec == nil { return nil, xiaomi.ErrNoSPEC } // 定位 intelligent-speaker 服务 var isSvc *miot.MIoTSpecService for _, svc := range spec.Services { if shortName(svc.Type) == "intelligent-speaker" { isSvc = svc d.isSiid = svc.IID break } } // 先用 resolver 解析通用属性 resolver := d.getSpecResolver() if resolver == nil { return nil, xiaomi.ErrNoSPEC } siid, piid, ok := resolver.find("on") if !ok { return nil, xiaomi.ErrNotSupported } d.propOn = siidPiid{siid, piid, true} if s, p, ok := resolver.find("volume"); ok { d.propVolume = siidPiid{s, p, true} } if s, p, ok := resolver.find("mute"); ok { d.propMute = siidPiid{s, p, true} } if s, p, ok := resolver.find("playing-state"); ok { d.propPlayState = siidPiid{s, p, true} } else if s, p, ok := resolver.find("tds-in"); ok { d.propPlayState = siidPiid{s, p, true} } // intelligent-speaker 属性 if isSvc != nil { for _, p := range isSvc.Properties { switch shortName(p.Type) { case "sleep-mode": d.propSleepMode = siidPiid{d.isSiid, p.PIID, true} case "audio-id": d.propAudioID = siidPiid{d.isSiid, p.PIID, true} } } // 动作 for _, a := range isSvc.Actions { switch shortName(a.Type) { case "wake-up": d.actWakeUp = isAction{d.isSiid, a.AIID} case "play-radio": d.actPlayRadio = isAction{d.isSiid, a.AIID} case "play-text": d.actPlayText = isAction{d.isSiid, a.AIID} case "execute-text-directive": d.actExecuteTextDirective = isAction{d.isSiid, a.AIID} case "play-music": d.actPlayMusic = isAction{d.isSiid, a.AIID} } } } return d, nil } func (d *speakerDevice) 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 *speakerDevice) 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 *speakerDevice) Toggle(ctx context.Context) error { s := d.GetProps() if s.On { return d.TurnOff(ctx) } return d.TurnOn(ctx) } func (d *speakerDevice) HasVolume() bool { return d.propVolume.valid } func (d *speakerDevice) HasMute() bool { return d.propMute.valid } func (d *speakerDevice) HasPlayControl() bool { return d.propPlayState.valid } func (d *speakerDevice) SetVolume(ctx context.Context, pct int) error { if !d.propVolume.valid { return xiaomi.ErrNotSupported } return d.SetProp(ctx, d.propVolume.siid, d.propVolume.piid, pct) } func (d *speakerDevice) GetVolume(ctx context.Context) (int, error) { if !d.propVolume.valid { return 0, xiaomi.ErrNotSupported } prop, err := d.GetProp(ctx, d.propVolume.siid, d.propVolume.piid) if err != nil { return 0, err } return toInt(prop.Value), nil } func (d *speakerDevice) SetMute(ctx context.Context, mute bool) error { if !d.propMute.valid { return xiaomi.ErrNotSupported } return d.SetProp(ctx, d.propMute.siid, d.propMute.piid, mute) } func (d *speakerDevice) GetMute(ctx context.Context) (bool, error) { if !d.propMute.valid { return false, xiaomi.ErrNotSupported } prop, err := d.GetProp(ctx, d.propMute.siid, d.propMute.piid) if err != nil { return false, err } if b, ok := prop.Value.(bool); ok { return b, nil } return false, nil } func (d *speakerDevice) SetPlayState(ctx context.Context, state int) error { if !d.propPlayState.valid { return xiaomi.ErrNotSupported } return d.SetProp(ctx, d.propPlayState.siid, d.propPlayState.piid, state) } func (d *speakerDevice) GetPlayState(ctx context.Context) (int, error) { if !d.propPlayState.valid { return 0, xiaomi.ErrNotSupported } prop, err := d.GetProp(ctx, d.propPlayState.siid, d.propPlayState.piid) if err != nil { return 0, err } return toInt(prop.Value), nil } // ── 智能音箱属性 ── func (d *speakerDevice) HasSleepMode() bool { return d.propSleepMode.valid } func (d *speakerDevice) SetSleepMode(ctx context.Context, sleep bool) error { if !d.propSleepMode.valid { return xiaomi.ErrNotSupported } return d.SetProp(ctx, d.propSleepMode.siid, d.propSleepMode.piid, sleep) } func (d *speakerDevice) GetSleepMode(ctx context.Context) (bool, error) { if !d.propSleepMode.valid { return false, xiaomi.ErrNotSupported } prop, err := d.GetProp(ctx, d.propSleepMode.siid, d.propSleepMode.piid) if err != nil { return false, err } if b, ok := prop.Value.(bool); ok { return b, nil } return false, nil } func (d *speakerDevice) GetAudioID(ctx context.Context) (string, error) { if !d.propAudioID.valid { return "", xiaomi.ErrNotSupported } prop, err := d.GetProp(ctx, d.propAudioID.siid, d.propAudioID.piid) if err != nil { return "", err } if s, ok := prop.Value.(string); ok { return s, nil } return "", nil } // ── 智能音箱动作 ── func (d *speakerDevice) WakeUp(ctx context.Context) error { if d.actWakeUp.siid == 0 { return xiaomi.ErrNotSupported } _, err := d.Action(ctx, d.actWakeUp.siid, d.actWakeUp.aiid, nil) return err } func (d *speakerDevice) PlayRadio(ctx context.Context) error { if d.actPlayRadio.siid == 0 { return xiaomi.ErrNotSupported } _, err := d.Action(ctx, d.actPlayRadio.siid, d.actPlayRadio.aiid, nil) return err } func (d *speakerDevice) PlayText(ctx context.Context, text string) error { if d.actPlayText.siid == 0 { return xiaomi.ErrNotSupported } _, err := d.Action(ctx, d.actPlayText.siid, d.actPlayText.aiid, []interface{}{ map[string]interface{}{"piid": 1, "value": text}, }) return err } func (d *speakerDevice) ExecuteTextDirective(ctx context.Context, text string, silent bool) error { if d.actExecuteTextDirective.siid == 0 { return xiaomi.ErrNotSupported } _, err := d.Action(ctx, d.actExecuteTextDirective.siid, d.actExecuteTextDirective.aiid, []interface{}{ map[string]interface{}{"piid": 1, "value": text}, map[string]interface{}{"piid": 2, "value": silent}, }) return err } func (d *speakerDevice) PlayMusic(ctx context.Context) error { if d.actPlayMusic.siid == 0 { return xiaomi.ErrNotSupported } _, err := d.Action(ctx, d.actPlayMusic.siid, d.actPlayMusic.aiid, nil) return err } // ── 订阅 / 状态 ── func (d *speakerDevice) OnPropsChanged(handler func(props SpeakerProps, changed string)) (string, error) { type field struct { siid, piid int name string fill func(*SpeakerProps, interface{}) } var fields []field add := func(sp siidPiid, name string, fill func(*SpeakerProps, interface{})) { if sp.valid { fields = append(fields, field{sp.siid, sp.piid, name, fill}) } } add(d.propOn, "on", func(s *SpeakerProps, v interface{}) { s.On, _ = v.(bool) }) add(d.propVolume, "volume", func(s *SpeakerProps, v interface{}) { s.Volume = toInt(v) }) add(d.propMute, "mute", func(s *SpeakerProps, v interface{}) { s.Mute, _ = v.(bool) }) add(d.propPlayState, "play-state", func(s *SpeakerProps, v interface{}) { s.PlayState = toInt(v) }) add(d.propSleepMode, "sleep-mode", func(s *SpeakerProps, v interface{}) { s.SleepMode, _ = v.(bool) }) add(d.propAudioID, "audio-id", func(s *SpeakerProps, v interface{}) { if str, ok := v.(string); ok { s.AudioID = str } }) 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.propsCache, prop.Value) handler(d.propsCache, f.name) return } } }) } func (d *speakerDevice) GetProps() SpeakerProps { return d.propsCache } func (d *speakerDevice) FetchProps(ctx context.Context) (SpeakerProps, error) { keys := []xiaomi.PropKey{} add := func(sp siidPiid) { if sp.valid { keys = append(keys, xiaomi.PropKey{DID: d.info.DID, SIID: sp.siid, PIID: sp.piid}) } } add(d.propOn) add(d.propVolume) add(d.propMute) add(d.propPlayState) add(d.propSleepMode) add(d.propAudioID) props, err := d.client.GetProps(ctx, keys) if err != nil { return d.propsCache, err } result := SpeakerProps{} for _, p := range props { switch { case match(d.propOn, p): result.On, _ = p.Value.(bool) case match(d.propVolume, p): result.Volume = toInt(p.Value) case match(d.propMute, p): result.Mute, _ = p.Value.(bool) case match(d.propPlayState, p): result.PlayState = toInt(p.Value) case match(d.propSleepMode, p): result.SleepMode, _ = p.Value.(bool) case match(d.propAudioID, p): if s, ok := p.Value.(string); ok { result.AudioID = s } } } d.propsCache = result return result, nil } func match(sp siidPiid, p *xiaomi.PropertyValue) bool { return sp.valid && p.SIID == sp.siid && p.PIID == sp.piid } // shortName extracts the short name from a SPEC URN. // E.g., "urn:miot-spec-v2:action:wake-up:..." → "wake-up". func shortName(urn string) string { parts := strings.Split(urn, ":") if len(parts) >= 4 { return parts[3] } return urn }