Files
Bose-SoundTouch/pkg/models/group.go
T
Tobias GesellchenandClaude Opus 4.7 cbbbaa9707 feat(group): add ST-10 stereo-pair support end-to-end
Implements the speaker-side group API surface (path 1 of the two
approaches gmuth outlined in issue #252): clients form, rename, and
dissolve stereo pairs directly on the device, and the resulting
GroupService.xml persists on disk in the same shape the device emits
over /getGroup.

What landed:

- pkg/models/group.go: Status field + IsEmpty() helper, matching the
  GET /getGroup response shape (id-attr, masterDeviceId, roles,
  senderIPAddress).
- pkg/client/client.go: GetGroup, AddGroup, UpdateGroup, RemoveGroup.
  The endpoint name is /getGroup (not /group, despite some wiki docs)
  — confirmed against a real ST-10's /supportedURLs. RemoveGroup uses
  GET per the wire spec.
- cmd/soundtouch-cli/cmd_group.go + main.go: new `group` subcommand
  with status / create --left --right [--name] / rename / remove,
  mirroring gmuth's group.sh recipe.

WebSocket notifications:

- pkg/models/websocket.go: EventTypeGroupUpdated +
  GroupUpdatedEvent + dispatch helpers. The device fans this out to
  both LEFT and RIGHT speakers on every group mutation, including
  empty-group teardowns; the parse test covers both shapes.
- pkg/client/websocket.go: OnGroupUpdated registration and dispatch.
- cmd/soundtouch-cli/cmd_events.go: `group` filter +
  handleGroupEvent formatter.

WebSocket observability (came up while validating the above against
a real device):

- New RawMessageHandler type + OnRawMessage hook that fires for every
  incoming frame before parsing, with the parse error alongside.
- New --debug flag on `events subscribe` with modes all / unknown /
  errors. Raw output goes to stderr so it composes cleanly with
  shell redirects.

The pkg/client refactor in this commit also adopts speaker.HTTPPort
(introduced in the previous refactor) — the unexported
defaultSoundTouchPort and three hard-coded 8090 literals are gone.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 23:18:08 +02:00

35 lines
1.1 KiB
Go

package models
import "encoding/xml"
// Group represents a stereo pair of two ST10 SoundTouch speakers.
type Group struct {
XMLName xml.Name `xml:"group"`
ID string `xml:"id,attr,omitempty"`
Name string `xml:"name"`
MasterDeviceID string `xml:"masterDeviceId"`
Roles GroupRoles `xml:"roles"`
SenderIPAddress string `xml:"senderIPAddress,omitempty"`
// Status is populated by the device on GET /group (e.g. "GROUP_OK")
// and omitted from requests we send back.
Status string `xml:"status,omitempty"`
}
// IsEmpty reports whether the device returned an empty <group/> element,
// which is the speaker's way of saying "no stereo pair configured".
func (g *Group) IsEmpty() bool {
return g.ID == "" && g.MasterDeviceID == "" && len(g.Roles.Roles) == 0
}
// GroupRoles contains the role assignments for devices in a group.
type GroupRoles struct {
Roles []GroupRole `xml:"groupRole"`
}
// GroupRole describes the role (LEFT or RIGHT) of a single device in a group.
type GroupRole struct {
DeviceID string `xml:"deviceId"`
Role string `xml:"role"`
IPAddress string `xml:"ipAddress,omitempty"`
}