Files
xiaomihome/xiaomi/devices/speaker.go
T
4566704 2b7de69403 refactor(logger): 日志改用 logrus 全局包级调用,移除 Logger 注入接口
为什么:并入宿主项目后须与宿主日志体系一致——宿主统一用 pkg/logger 初始化
logrus 全局实例并直接 logrus.Xxx 包级调用;包一层 Logger 接口会让 logrus 的
caller 定位(报告调用文件:行号)失准。

改动:
- 删除 logger 包(Logger 接口 / Default / SetDefault,默认本就是 logrus.StandardLogger)
- 11 个结构体移除 lgr logger.Logger 字段、SetLogger 方法及构造中的 lgr 初始化
- 192 处 lgr.Xxxf 调用改为 logrus.Xxxf,日志消息文本保持不变
- xiaomi.Client 移除 Logger() 访问器,air_conditioner 回读日志去掉判空调用直连 logrus
- miot_i18n.go 两处标准库 log.Printf 改为 logrus.Errorf,统一日志出口
- go.mod:logrus 从 indirect 提升为直接依赖
- goimports/goformat 全树规范化:此前部分文件未 gofmt(单行 if、对齐),本次顺带
  格式化,纯空白/换行变化,无逻辑改动

验证:go build / go vet / go test ./... 全部通过(bridge、miot、xiaomi、devices、specs)
2026-09-15 20:16:26 +08:00

435 lines
12 KiB
Go

package devices
import (
"context"
"strings"
"xiaomihome/miot"
"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
}