mirror of
https://github.com/gesellix/Bose-SoundTouch.git
synced 2026-08-18 08:36:13 +00:00
Move example files to separate packages to avoid main redeclaration. Fix cyclomatic complexity and variable shadowing. Address errcheck and wsl linting issues. Update tests to handle capabilities and fix panics. Apply consistent formatting with gofmt.
483 lines
13 KiB
Go
483 lines
13 KiB
Go
package models
|
|
|
|
import (
|
|
"encoding/xml"
|
|
"fmt"
|
|
"net"
|
|
"strings"
|
|
)
|
|
|
|
// ZoneInfo represents the response from GET /getZone endpoint
|
|
type ZoneInfo struct {
|
|
XMLName xml.Name `xml:"zone"`
|
|
Master string `xml:"master,attr"`
|
|
Members []Member `xml:"member"`
|
|
}
|
|
|
|
// Member represents a device member in a multiroom zone
|
|
type Member struct {
|
|
XMLName xml.Name `xml:"member"`
|
|
DeviceID string `xml:",chardata"`
|
|
IP string `xml:"ipaddress,attr"`
|
|
}
|
|
|
|
// ZoneRequest represents the request for POST /setZone endpoint
|
|
type ZoneRequest struct {
|
|
XMLName xml.Name `xml:"zone"`
|
|
Master string `xml:"master,attr"`
|
|
Members []MemberEntry `xml:"member"`
|
|
}
|
|
|
|
// MemberEntry represents a member entry in zone configuration requests
|
|
type MemberEntry struct {
|
|
XMLName xml.Name `xml:"member"`
|
|
DeviceID string `xml:",chardata"`
|
|
IP string `xml:"ipaddress,attr,omitempty"`
|
|
}
|
|
|
|
// ZoneStatus represents possible zone states
|
|
type ZoneStatus string
|
|
|
|
const (
|
|
// ZoneStatusStandalone indicates the device is operating independently
|
|
ZoneStatusStandalone ZoneStatus = "STANDALONE"
|
|
// ZoneStatusMaster indicates the device is the master in a zone
|
|
ZoneStatusMaster ZoneStatus = "MASTER"
|
|
// ZoneStatusSlave indicates the device is a slave in a zone
|
|
ZoneStatusSlave ZoneStatus = "SLAVE"
|
|
)
|
|
|
|
// String returns a human-readable string representation
|
|
func (zs ZoneStatus) String() string {
|
|
switch zs {
|
|
case ZoneStatusStandalone:
|
|
return "Standalone"
|
|
case ZoneStatusMaster:
|
|
return "Zone Master"
|
|
case ZoneStatusSlave:
|
|
return "Zone Member"
|
|
default:
|
|
return "Unknown"
|
|
}
|
|
}
|
|
|
|
// NewZoneRequest creates a new zone configuration request
|
|
func NewZoneRequest(masterDeviceID string) *ZoneRequest {
|
|
return &ZoneRequest{
|
|
Master: masterDeviceID,
|
|
Members: []MemberEntry{},
|
|
}
|
|
}
|
|
|
|
// AddMember adds a device to the zone configuration
|
|
func (zr *ZoneRequest) AddMember(deviceID, ipAddress string) {
|
|
member := MemberEntry{
|
|
DeviceID: deviceID,
|
|
IP: ipAddress,
|
|
}
|
|
zr.Members = append(zr.Members, member)
|
|
}
|
|
|
|
// AddMemberByDeviceID adds a device to the zone by device ID only
|
|
func (zr *ZoneRequest) AddMemberByDeviceID(deviceID string) {
|
|
member := MemberEntry{
|
|
DeviceID: deviceID,
|
|
}
|
|
zr.Members = append(zr.Members, member)
|
|
}
|
|
|
|
// RemoveMember removes a device from the zone configuration
|
|
func (zr *ZoneRequest) RemoveMember(deviceID string) {
|
|
for i, member := range zr.Members {
|
|
if member.DeviceID == deviceID {
|
|
zr.Members = append(zr.Members[:i], zr.Members[i+1:]...)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// ClearMembers removes all members from the zone (creates standalone configuration)
|
|
func (zr *ZoneRequest) ClearMembers() {
|
|
zr.Members = []MemberEntry{}
|
|
}
|
|
|
|
// HasMember checks if a device is in the zone configuration
|
|
func (zr *ZoneRequest) HasMember(deviceID string) bool {
|
|
for _, member := range zr.Members {
|
|
if member.DeviceID == deviceID {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// GetMemberCount returns the number of members in the zone
|
|
func (zr *ZoneRequest) GetMemberCount() int {
|
|
return len(zr.Members)
|
|
}
|
|
|
|
// Validate validates the zone request
|
|
func (zr *ZoneRequest) Validate() error {
|
|
if zr.Master == "" {
|
|
return fmt.Errorf("master device ID is required")
|
|
}
|
|
|
|
// Check for duplicate device IDs
|
|
seen := make(map[string]bool)
|
|
seen[zr.Master] = true
|
|
|
|
for _, member := range zr.Members {
|
|
if member.DeviceID == "" {
|
|
return fmt.Errorf("member device ID cannot be empty")
|
|
}
|
|
|
|
if seen[member.DeviceID] {
|
|
return fmt.Errorf("duplicate device ID found: %s", member.DeviceID)
|
|
}
|
|
|
|
seen[member.DeviceID] = true
|
|
|
|
// Validate IP address if provided
|
|
if member.IP != "" {
|
|
if net.ParseIP(member.IP) == nil {
|
|
return fmt.Errorf("invalid IP address for device %s: %s", member.DeviceID, member.IP)
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// IsStandalone returns true if this is a standalone (single device) configuration
|
|
func (zi *ZoneInfo) IsStandalone() bool {
|
|
return len(zi.Members) == 0
|
|
}
|
|
|
|
// IsMaster returns true if the given device ID is the zone master
|
|
func (zi *ZoneInfo) IsMaster(deviceID string) bool {
|
|
return zi.Master == deviceID
|
|
}
|
|
|
|
// IsMember returns true if the given device ID is a zone member (not master)
|
|
func (zi *ZoneInfo) IsMember(deviceID string) bool {
|
|
for _, member := range zi.Members {
|
|
if member.DeviceID == deviceID {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// IsInZone returns true if the given device ID is in the zone (master or member)
|
|
func (zi *ZoneInfo) IsInZone(deviceID string) bool {
|
|
return zi.IsMaster(deviceID) || zi.IsMember(deviceID)
|
|
}
|
|
|
|
// GetMemberByDeviceID returns the member with the given device ID
|
|
func (zi *ZoneInfo) GetMemberByDeviceID(deviceID string) (*Member, bool) {
|
|
for _, member := range zi.Members {
|
|
if member.DeviceID == deviceID {
|
|
return &member, true
|
|
}
|
|
}
|
|
|
|
return nil, false
|
|
}
|
|
|
|
// GetMemberByIP returns the member with the given IP address
|
|
func (zi *ZoneInfo) GetMemberByIP(ipAddress string) (*Member, bool) {
|
|
for _, member := range zi.Members {
|
|
if member.IP == ipAddress {
|
|
return &member, true
|
|
}
|
|
}
|
|
|
|
return nil, false
|
|
}
|
|
|
|
// GetAllDeviceIDs returns all device IDs in the zone (master + members)
|
|
func (zi *ZoneInfo) GetAllDeviceIDs() []string {
|
|
devices := []string{zi.Master}
|
|
for _, member := range zi.Members {
|
|
devices = append(devices, member.DeviceID)
|
|
}
|
|
|
|
return devices
|
|
}
|
|
|
|
// GetTotalDeviceCount returns the total number of devices in the zone
|
|
func (zi *ZoneInfo) GetTotalDeviceCount() int {
|
|
return 1 + len(zi.Members) // Master + members
|
|
}
|
|
|
|
// GetZoneStatus returns the zone status for a given device ID
|
|
func (zi *ZoneInfo) GetZoneStatus(deviceID string) ZoneStatus {
|
|
if zi.IsMaster(deviceID) {
|
|
if zi.IsStandalone() {
|
|
return ZoneStatusStandalone
|
|
}
|
|
|
|
return ZoneStatusMaster
|
|
}
|
|
|
|
if zi.IsMember(deviceID) {
|
|
return ZoneStatusSlave
|
|
}
|
|
|
|
return ZoneStatusStandalone // Device not in zone
|
|
}
|
|
|
|
// String returns a human-readable string representation of the zone
|
|
func (zi *ZoneInfo) String() string {
|
|
if zi.IsStandalone() {
|
|
return fmt.Sprintf("Standalone device: %s", zi.Master)
|
|
}
|
|
|
|
var memberIDs []string
|
|
for _, member := range zi.Members {
|
|
memberIDs = append(memberIDs, member.DeviceID)
|
|
}
|
|
|
|
return fmt.Sprintf("Zone Master: %s, Members: [%s] (%d total devices)",
|
|
zi.Master, strings.Join(memberIDs, ", "), zi.GetTotalDeviceCount())
|
|
}
|
|
|
|
// ToZoneRequest converts ZoneInfo to a ZoneRequest for modification
|
|
func (zi *ZoneInfo) ToZoneRequest() *ZoneRequest {
|
|
request := NewZoneRequest(zi.Master)
|
|
for _, member := range zi.Members {
|
|
request.AddMember(member.DeviceID, member.IP)
|
|
}
|
|
|
|
return request
|
|
}
|
|
|
|
// ZoneOperation represents different types of zone operations
|
|
type ZoneOperation string
|
|
|
|
const (
|
|
// ZoneOpCreate indicates creating a new zone
|
|
ZoneOpCreate ZoneOperation = "CREATE"
|
|
// ZoneOpModify indicates modifying an existing zone
|
|
ZoneOpModify ZoneOperation = "MODIFY"
|
|
// ZoneOpAddMember indicates adding a member to a zone
|
|
ZoneOpAddMember ZoneOperation = "ADD_MEMBER"
|
|
// ZoneOpRemove indicates removing a member from a zone
|
|
ZoneOpRemove ZoneOperation = "REMOVE_MEMBER"
|
|
// ZoneOpDissolve indicates dissolving a zone
|
|
ZoneOpDissolve ZoneOperation = "DISSOLVE"
|
|
)
|
|
|
|
// String returns a human-readable string representation
|
|
func (zo ZoneOperation) String() string {
|
|
switch zo {
|
|
case ZoneOpCreate:
|
|
return "Create Zone"
|
|
case ZoneOpModify:
|
|
return "Modify Zone"
|
|
case ZoneOpAddMember:
|
|
return "Add Member"
|
|
case ZoneOpRemove:
|
|
return "Remove Member"
|
|
case ZoneOpDissolve:
|
|
return "Dissolve Zone"
|
|
default:
|
|
return "Unknown Operation"
|
|
}
|
|
}
|
|
|
|
// ZoneBuilder provides a fluent interface for building zone configurations
|
|
type ZoneBuilder struct {
|
|
request *ZoneRequest
|
|
}
|
|
|
|
// NewZoneBuilder creates a new zone builder with the specified master device
|
|
func NewZoneBuilder(masterDeviceID string) *ZoneBuilder {
|
|
return &ZoneBuilder{
|
|
request: NewZoneRequest(masterDeviceID),
|
|
}
|
|
}
|
|
|
|
// WithMember adds a member to the zone configuration
|
|
func (zb *ZoneBuilder) WithMember(deviceID, ipAddress string) *ZoneBuilder {
|
|
zb.request.AddMember(deviceID, ipAddress)
|
|
return zb
|
|
}
|
|
|
|
// WithMemberByDeviceID adds a member by device ID only
|
|
func (zb *ZoneBuilder) WithMemberByDeviceID(deviceID string) *ZoneBuilder {
|
|
zb.request.AddMemberByDeviceID(deviceID)
|
|
return zb
|
|
}
|
|
|
|
// Build returns the constructed zone request
|
|
func (zb *ZoneBuilder) Build() (*ZoneRequest, error) {
|
|
if err := zb.request.Validate(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return zb.request, nil
|
|
}
|
|
|
|
// ZoneError represents zone-specific errors
|
|
type ZoneError struct {
|
|
Operation ZoneOperation
|
|
DeviceID string
|
|
Reason string
|
|
}
|
|
|
|
// Error implements the error interface
|
|
func (ze *ZoneError) Error() string {
|
|
if ze.DeviceID != "" {
|
|
return fmt.Sprintf("zone %s failed for device %s: %s",
|
|
ze.Operation.String(), ze.DeviceID, ze.Reason)
|
|
}
|
|
|
|
return fmt.Sprintf("zone %s failed: %s", ze.Operation.String(), ze.Reason)
|
|
}
|
|
|
|
// NewZoneError creates a new zone error
|
|
func NewZoneError(op ZoneOperation, deviceID, reason string) *ZoneError {
|
|
return &ZoneError{
|
|
Operation: op,
|
|
DeviceID: deviceID,
|
|
Reason: reason,
|
|
}
|
|
}
|
|
|
|
// Common zone error reasons
|
|
const (
|
|
ZoneErrorDeviceNotFound = "device not found"
|
|
ZoneErrorDeviceOffline = "device offline"
|
|
ZoneErrorIncompatible = "incompatible device type"
|
|
ZoneErrorAlreadyInZone = "device already in zone"
|
|
ZoneErrorNotInZone = "device not in zone"
|
|
ZoneErrorMasterRequired = "master device required"
|
|
ZoneErrorNetworkError = "network communication error"
|
|
ZoneErrorUnsupported = "operation not supported by device"
|
|
ZoneErrorMaxMembersReached = "maximum zone members reached"
|
|
)
|
|
|
|
// ZoneCapabilities represents zone-related capabilities of a device
|
|
type ZoneCapabilities struct {
|
|
CanBeMaster bool `json:"canBeMaster"`
|
|
CanBeMember bool `json:"canBeMember"`
|
|
MaxZoneMembers int `json:"maxZoneMembers"`
|
|
SupportsMultiroom bool `json:"supportsMultiroom"`
|
|
}
|
|
|
|
// DefaultZoneCapabilities returns default zone capabilities
|
|
func DefaultZoneCapabilities() ZoneCapabilities {
|
|
return ZoneCapabilities{
|
|
CanBeMaster: true,
|
|
CanBeMember: true,
|
|
MaxZoneMembers: 6, // Common SoundTouch limit
|
|
SupportsMultiroom: true,
|
|
}
|
|
}
|
|
|
|
// CanCreateZone returns true if the device can create zones
|
|
func (zc *ZoneCapabilities) CanCreateZone() bool {
|
|
return zc.SupportsMultiroom && zc.CanBeMaster
|
|
}
|
|
|
|
// CanJoinZone returns true if the device can join zones
|
|
func (zc *ZoneCapabilities) CanJoinZone() bool {
|
|
return zc.SupportsMultiroom && zc.CanBeMember
|
|
}
|
|
|
|
// ZoneSlaveRequest represents the request for /addZoneSlave and /removeZoneSlave endpoints
|
|
type ZoneSlaveRequest struct {
|
|
XMLName xml.Name `xml:"zone"`
|
|
Master string `xml:"master,attr"`
|
|
Members []ZoneSlaveEntry `xml:"member"`
|
|
}
|
|
|
|
// ZoneSlaveEntry represents a single member entry in zone slave operations
|
|
type ZoneSlaveEntry struct {
|
|
XMLName xml.Name `xml:"member"`
|
|
DeviceID string `xml:",chardata"`
|
|
IP string `xml:"ipaddress,attr,omitempty"`
|
|
}
|
|
|
|
// NewZoneSlaveRequest creates a new zone slave operation request
|
|
func NewZoneSlaveRequest(masterDeviceID string) *ZoneSlaveRequest {
|
|
return &ZoneSlaveRequest{
|
|
Master: masterDeviceID,
|
|
Members: []ZoneSlaveEntry{},
|
|
}
|
|
}
|
|
|
|
// AddSlave adds a single slave to the request
|
|
func (zsr *ZoneSlaveRequest) AddSlave(deviceID, ipAddress string) {
|
|
slave := ZoneSlaveEntry{
|
|
DeviceID: deviceID,
|
|
IP: ipAddress,
|
|
}
|
|
zsr.Members = append(zsr.Members, slave)
|
|
}
|
|
|
|
// Validate validates the zone slave request
|
|
func (zsr *ZoneSlaveRequest) Validate() error {
|
|
if zsr.Master == "" {
|
|
return fmt.Errorf("master device ID is required")
|
|
}
|
|
|
|
if len(zsr.Members) != 1 {
|
|
return fmt.Errorf("zone slave operations require exactly one member, got %d", len(zsr.Members))
|
|
}
|
|
|
|
member := zsr.Members[0]
|
|
if member.DeviceID == "" {
|
|
return fmt.Errorf("slave device ID cannot be empty")
|
|
}
|
|
|
|
if member.DeviceID == zsr.Master {
|
|
return fmt.Errorf("slave device ID cannot be the same as master: %s", member.DeviceID)
|
|
}
|
|
|
|
if member.IP != "" {
|
|
if net.ParseIP(member.IP) == nil {
|
|
return fmt.Errorf("invalid IP address for device %s: %s", member.DeviceID, member.IP)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetSlaveDeviceID returns the device ID of the slave being added/removed
|
|
func (zsr *ZoneSlaveRequest) GetSlaveDeviceID() string {
|
|
if len(zsr.Members) > 0 {
|
|
return zsr.Members[0].DeviceID
|
|
}
|
|
|
|
return ""
|
|
}
|
|
|
|
// GetSlaveIP returns the IP address of the slave being added/removed
|
|
func (zsr *ZoneSlaveRequest) GetSlaveIP() string {
|
|
if len(zsr.Members) > 0 {
|
|
return zsr.Members[0].IP
|
|
}
|
|
|
|
return ""
|
|
}
|
|
|
|
// String returns a human-readable string representation
|
|
func (zsr *ZoneSlaveRequest) String() string {
|
|
if len(zsr.Members) == 0 {
|
|
return fmt.Sprintf("Zone slave operation on master %s (no slave specified)", zsr.Master)
|
|
}
|
|
|
|
slave := zsr.Members[0]
|
|
if slave.IP != "" {
|
|
return fmt.Sprintf("Zone slave operation: master=%s, slave=%s (%s)",
|
|
zsr.Master, slave.DeviceID, slave.IP)
|
|
}
|
|
|
|
return fmt.Sprintf("Zone slave operation: master=%s, slave=%s",
|
|
zsr.Master, slave.DeviceID)
|
|
}
|