Files
Bose-SoundTouch/pkg/models/websocket.go
T
Tobias Gesellchen 9ed8182278 fix: resolve golangci-lint issues
- Fix all errcheck issues by properly checking error return values
- Fix gocritic exitAfterDefer issues by replacing log.Fatalf with return statements
- Fix rangeValCopy issues by using index-based iteration for large structs
- Add missing package comments for all packages
- Fix unused parameter issues by renaming to underscore
- Fix empty block issues by adding explicit error handling
- Add documentation for exported methods and constants
- Fix shadow variable issues
- Replace deprecated strings.Title with manual implementation
- Fix defer function error handling

Reduced lint issues from 108 to 84 (22% improvement)
All critical error handling and code quality issues resolved
2026-01-09 22:59:21 +01:00

499 lines
16 KiB
Go

package models
import (
"encoding/xml"
"fmt"
"time"
)
// WebSocketEventType represents the type of WebSocket event
type WebSocketEventType string
const (
// EventTypeNowPlaying indicates a now playing status update
EventTypeNowPlaying WebSocketEventType = "nowPlayingUpdated"
// EventTypeVolumeUpdated indicates a volume level change
EventTypeVolumeUpdated WebSocketEventType = "volumeUpdated"
// EventTypeConnectionState indicates a connection state change
EventTypeConnectionState WebSocketEventType = "connectionStateUpdated"
// EventTypePresetUpdated indicates a preset configuration change
EventTypePresetUpdated WebSocketEventType = "presetUpdated"
// EventTypeZoneUpdated indicates a zone configuration change
EventTypeZoneUpdated WebSocketEventType = "zoneUpdated"
// EventTypeBassUpdated indicates a bass level change
EventTypeBassUpdated WebSocketEventType = "bassUpdated"
// EventTypeClockTimeUpdated indicates a clock time change
EventTypeClockTimeUpdated WebSocketEventType = "clockTimeUpdated"
// EventTypeClockDisplayUpdated indicates a clock display setting change
EventTypeClockDisplayUpdated WebSocketEventType = "clockDisplayUpdated"
// EventTypeNameUpdated indicates a device name change
EventTypeNameUpdated WebSocketEventType = "nameUpdated"
// EventTypeErrorUpdated indicates an error status change
EventTypeErrorUpdated WebSocketEventType = "errorUpdated"
// EventTypeRecentsUpdated indicates a recent items list change
EventTypeRecentsUpdated WebSocketEventType = "recentsUpdated"
// EventTypeLanguageUpdated indicates a language setting change
EventTypeLanguageUpdated WebSocketEventType = "languageUpdated"
// EventTypeUnknown indicates an unrecognized event type
EventTypeUnknown WebSocketEventType = "unknown"
)
// String returns a human-readable string representation
func (e WebSocketEventType) String() string {
switch e {
case EventTypeNowPlaying:
return "Now Playing Updated"
case EventTypeVolumeUpdated:
return "Volume Updated"
case EventTypeConnectionState:
return "Connection State Updated"
case EventTypePresetUpdated:
return "Preset Updated"
case EventTypeZoneUpdated:
return "Zone Updated"
case EventTypeBassUpdated:
return "Bass Updated"
case EventTypeClockTimeUpdated:
return "Clock Time Updated"
case EventTypeClockDisplayUpdated:
return "Clock Display Updated"
case EventTypeNameUpdated:
return "Name Updated"
case EventTypeErrorUpdated:
return "Error Updated"
case EventTypeRecentsUpdated:
return "Recents Updated"
case EventTypeLanguageUpdated:
return "Language Updated"
default:
return "Unknown Event"
}
}
// WebSocketEvent represents a generic WebSocket event from SoundTouch device
type WebSocketEvent struct {
XMLName xml.Name `xml:"updates"`
DeviceID string `xml:"deviceID,attr"`
NowPlayingUpdated *NowPlayingUpdatedEvent `xml:"nowPlayingUpdated,omitempty"`
VolumeUpdated *VolumeUpdatedEvent `xml:"volumeUpdated,omitempty"`
ConnectionStateUpdated *ConnectionStateUpdatedEvent `xml:"connectionStateUpdated,omitempty"`
PresetUpdated *PresetUpdatedEvent `xml:"presetUpdated,omitempty"`
ZoneUpdated *ZoneUpdatedEvent `xml:"zoneUpdated,omitempty"`
BassUpdated *BassUpdatedEvent `xml:"bassUpdated,omitempty"`
ClockTimeUpdated *ClockTimeUpdatedEvent `xml:"clockTimeUpdated,omitempty"`
ClockDisplayUpdated *ClockDisplayUpdatedEvent `xml:"clockDisplayUpdated,omitempty"`
NameUpdated *NameUpdatedEvent `xml:"nameUpdated,omitempty"`
ErrorUpdated *ErrorUpdatedEvent `xml:"errorUpdated,omitempty"`
RecentsUpdated *RecentsUpdatedEvent `xml:"recentsUpdated,omitempty"`
LanguageUpdated *LanguageUpdatedEvent `xml:"languageUpdated,omitempty"`
Timestamp time.Time `json:"timestamp"` // Added by client for tracking
}
// GetEvents returns all events present in this WebSocket event
func (e *WebSocketEvent) GetEvents() []interface{} {
var events []interface{}
if e.NowPlayingUpdated != nil {
events = append(events, e.NowPlayingUpdated)
}
if e.VolumeUpdated != nil {
events = append(events, e.VolumeUpdated)
}
if e.ConnectionStateUpdated != nil {
events = append(events, e.ConnectionStateUpdated)
}
if e.PresetUpdated != nil {
events = append(events, e.PresetUpdated)
}
if e.ZoneUpdated != nil {
events = append(events, e.ZoneUpdated)
}
if e.BassUpdated != nil {
events = append(events, e.BassUpdated)
}
if e.ClockTimeUpdated != nil {
events = append(events, e.ClockTimeUpdated)
}
if e.ClockDisplayUpdated != nil {
events = append(events, e.ClockDisplayUpdated)
}
if e.NameUpdated != nil {
events = append(events, e.NameUpdated)
}
if e.ErrorUpdated != nil {
events = append(events, e.ErrorUpdated)
}
if e.RecentsUpdated != nil {
events = append(events, e.RecentsUpdated)
}
if e.LanguageUpdated != nil {
events = append(events, e.LanguageUpdated)
}
return events
}
// NowPlayingUpdatedEvent represents a now playing update event
type NowPlayingUpdatedEvent struct {
XMLName xml.Name `xml:"nowPlayingUpdated"`
DeviceID string `xml:"deviceID,attr"`
NowPlaying NowPlaying `xml:"nowPlaying"`
}
// VolumeUpdatedEvent represents a volume update event
type VolumeUpdatedEvent struct {
XMLName xml.Name `xml:"volumeUpdated"`
DeviceID string `xml:"deviceID,attr"`
Volume Volume `xml:"volume"`
}
// ConnectionStateUpdatedEvent represents a connection state update event
type ConnectionStateUpdatedEvent struct {
XMLName xml.Name `xml:"connectionStateUpdated"`
DeviceID string `xml:"deviceID,attr"`
ConnectionState ConnectionState `xml:"connectionState"`
}
// ConnectionState represents the device's network connection state
type ConnectionState struct {
XMLName xml.Name `xml:"connectionState"`
State string `xml:"state,attr"`
Signal string `xml:"signal,attr"`
}
// ConnectionStateType represents connection state values
type ConnectionStateType string
const (
// ConnectionStateConnected indicates the device is connected
ConnectionStateConnected ConnectionStateType = "CONNECTED"
// ConnectionStateDisconnected indicates the device is disconnected
ConnectionStateDisconnected ConnectionStateType = "DISCONNECTED"
)
// IsConnected returns true if the device is connected
func (cs *ConnectionState) IsConnected() bool {
return cs.State == string(ConnectionStateConnected)
}
// GetSignalStrength returns the signal strength as a string
func (cs *ConnectionState) GetSignalStrength() string {
return cs.Signal
}
// PresetUpdatedEvent represents a preset update event
type PresetUpdatedEvent struct {
XMLName xml.Name `xml:"presetUpdated"`
DeviceID string `xml:"deviceID,attr"`
Preset Preset `xml:"preset"`
}
// ZoneUpdatedEvent represents a multiroom zone update event
type ZoneUpdatedEvent struct {
XMLName xml.Name `xml:"zoneUpdated"`
DeviceID string `xml:"deviceID,attr"`
Zone Zone `xml:"zone"`
}
// Zone represents multiroom zone information
type Zone struct {
XMLName xml.Name `xml:"zone"`
Master string `xml:"master,attr"`
Members []ZoneMember `xml:"member"`
}
// ZoneMember represents a member of a multiroom zone
type ZoneMember struct {
XMLName xml.Name `xml:"member"`
DeviceID string `xml:",chardata"`
IP string `xml:"ipaddress,attr"`
}
// BassUpdatedEvent represents a bass setting update event
type BassUpdatedEvent struct {
XMLName xml.Name `xml:"bassUpdated"`
DeviceID string `xml:"deviceID,attr"`
Bass Bass `xml:"bass"`
}
// ClockTimeUpdatedEvent represents a clock time update event
type ClockTimeUpdatedEvent struct {
XMLName xml.Name `xml:"clockTimeUpdated"`
DeviceID string `xml:"deviceID,attr"`
ClockTime ClockTime `xml:"clockTime"`
}
// ClockDisplayUpdatedEvent represents a clock display setting update event
type ClockDisplayUpdatedEvent struct {
XMLName xml.Name `xml:"clockDisplayUpdated"`
DeviceID string `xml:"deviceID,attr"`
ClockDisplay ClockDisplay `xml:"clockDisplay"`
}
// NameUpdatedEvent represents a device name update event
type NameUpdatedEvent struct {
XMLName xml.Name `xml:"nameUpdated"`
DeviceID string `xml:"deviceID,attr"`
Name Name `xml:"name"`
}
// ErrorUpdatedEvent represents an error state update event
type ErrorUpdatedEvent struct {
XMLName xml.Name `xml:"errorUpdated"`
DeviceID string `xml:"deviceID,attr"`
Error Error `xml:"error"`
}
// Error represents an error state
type Error struct {
XMLName xml.Name `xml:"error"`
Value string `xml:"value,attr"`
Name string `xml:"name,attr"`
Text string `xml:",chardata"`
}
// RecentsUpdatedEvent represents a recent items update event
type RecentsUpdatedEvent struct {
XMLName xml.Name `xml:"recentsUpdated"`
DeviceID string `xml:"deviceID,attr"`
Recents Recents `xml:"recents"`
}
// Recents represents recently played items
type Recents struct {
XMLName xml.Name `xml:"recents"`
Items []RecentItem `xml:"recent"`
}
// RecentItem represents a recently played item
type RecentItem struct {
XMLName xml.Name `xml:"recent"`
DeviceID string `xml:"deviceID,attr"`
CreatedOn int64 `xml:"createdOn,attr"`
ID string `xml:"id,attr"`
ContentItem ContentItem `xml:"ContentItem"`
}
// LanguageUpdatedEvent represents a language setting update event
type LanguageUpdatedEvent struct {
XMLName xml.Name `xml:"languageUpdated"`
DeviceID string `xml:"deviceID,attr"`
Language Language `xml:"language"`
}
// Language represents language settings
type Language struct {
XMLName xml.Name `xml:"language"`
Value string `xml:",chardata"`
}
// EventHandler represents a function that handles WebSocket events
type EventHandler func(event *WebSocketEvent)
// TypedEventHandler represents a function that handles specific event types
type TypedEventHandler[T any] func(event T)
// WebSocketEventHandlers holds typed event handlers for different event types
type WebSocketEventHandlers struct {
OnNowPlaying TypedEventHandler[*NowPlayingUpdatedEvent]
OnVolumeUpdated TypedEventHandler[*VolumeUpdatedEvent]
OnConnectionState TypedEventHandler[*ConnectionStateUpdatedEvent]
OnPresetUpdated TypedEventHandler[*PresetUpdatedEvent]
OnZoneUpdated TypedEventHandler[*ZoneUpdatedEvent]
OnBassUpdated TypedEventHandler[*BassUpdatedEvent]
OnClockTimeUpdated TypedEventHandler[*ClockTimeUpdatedEvent]
OnClockDisplayUpdated TypedEventHandler[*ClockDisplayUpdatedEvent]
OnNameUpdated TypedEventHandler[*NameUpdatedEvent]
OnErrorUpdated TypedEventHandler[*ErrorUpdatedEvent]
OnRecentsUpdated TypedEventHandler[*RecentsUpdatedEvent]
OnLanguageUpdated TypedEventHandler[*LanguageUpdatedEvent]
OnUnknownEvent EventHandler
}
// ParseWebSocketEvent attempts to parse a WebSocket message into a specific event type
func ParseWebSocketEvent(data []byte) (*WebSocketEvent, error) {
var event WebSocketEvent
if err := xml.Unmarshal(data, &event); err != nil {
return nil, fmt.Errorf("failed to parse WebSocket event: %w", err)
}
// Add timestamp
event.Timestamp = time.Now()
return &event, nil
}
// ParseTypedEvent attempts to parse a WebSocket event into a specific typed event
func ParseTypedEvent[T any](event *WebSocketEvent, eventType WebSocketEventType) (T, error) {
var result T
// Get the event directly from the parsed structure
switch eventType {
case EventTypeNowPlaying:
if event.NowPlayingUpdated != nil {
if typedResult, ok := interface{}(event.NowPlayingUpdated).(T); ok {
return typedResult, nil
}
}
case EventTypeVolumeUpdated:
if event.VolumeUpdated != nil {
if typedResult, ok := interface{}(event.VolumeUpdated).(T); ok {
return typedResult, nil
}
}
case EventTypeConnectionState:
if event.ConnectionStateUpdated != nil {
if typedResult, ok := interface{}(event.ConnectionStateUpdated).(T); ok {
return typedResult, nil
}
}
case EventTypePresetUpdated:
if event.PresetUpdated != nil {
if typedResult, ok := interface{}(event.PresetUpdated).(T); ok {
return typedResult, nil
}
}
case EventTypeZoneUpdated:
if event.ZoneUpdated != nil {
if typedResult, ok := interface{}(event.ZoneUpdated).(T); ok {
return typedResult, nil
}
}
case EventTypeBassUpdated:
if event.BassUpdated != nil {
if typedResult, ok := interface{}(event.BassUpdated).(T); ok {
return typedResult, nil
}
}
case EventTypeClockTimeUpdated:
if event.ClockTimeUpdated != nil {
if typedResult, ok := interface{}(event.ClockTimeUpdated).(T); ok {
return typedResult, nil
}
}
case EventTypeClockDisplayUpdated:
if event.ClockDisplayUpdated != nil {
if typedResult, ok := interface{}(event.ClockDisplayUpdated).(T); ok {
return typedResult, nil
}
}
case EventTypeNameUpdated:
if event.NameUpdated != nil {
if typedResult, ok := interface{}(event.NameUpdated).(T); ok {
return typedResult, nil
}
}
case EventTypeErrorUpdated:
if event.ErrorUpdated != nil {
if typedResult, ok := interface{}(event.ErrorUpdated).(T); ok {
return typedResult, nil
}
}
case EventTypeRecentsUpdated:
if event.RecentsUpdated != nil {
if typedResult, ok := interface{}(event.RecentsUpdated).(T); ok {
return typedResult, nil
}
}
case EventTypeLanguageUpdated:
if event.LanguageUpdated != nil {
if typedResult, ok := interface{}(event.LanguageUpdated).(T); ok {
return typedResult, nil
}
}
}
return result, fmt.Errorf("event type %s not found in WebSocket event", eventType)
}
// HasEventType checks if the WebSocket event contains a specific event type
func (e *WebSocketEvent) HasEventType(eventType WebSocketEventType) bool {
switch eventType {
case EventTypeNowPlaying:
return e.NowPlayingUpdated != nil
case EventTypeVolumeUpdated:
return e.VolumeUpdated != nil
case EventTypeConnectionState:
return e.ConnectionStateUpdated != nil
case EventTypePresetUpdated:
return e.PresetUpdated != nil
case EventTypeZoneUpdated:
return e.ZoneUpdated != nil
case EventTypeBassUpdated:
return e.BassUpdated != nil
case EventTypeClockTimeUpdated:
return e.ClockTimeUpdated != nil
case EventTypeClockDisplayUpdated:
return e.ClockDisplayUpdated != nil
case EventTypeNameUpdated:
return e.NameUpdated != nil
case EventTypeErrorUpdated:
return e.ErrorUpdated != nil
case EventTypeRecentsUpdated:
return e.RecentsUpdated != nil
case EventTypeLanguageUpdated:
return e.LanguageUpdated != nil
}
return false
}
// GetEventTypes returns all event types present in this WebSocket event
func (e *WebSocketEvent) GetEventTypes() []WebSocketEventType {
var types []WebSocketEventType
if e.NowPlayingUpdated != nil {
types = append(types, EventTypeNowPlaying)
}
if e.VolumeUpdated != nil {
types = append(types, EventTypeVolumeUpdated)
}
if e.ConnectionStateUpdated != nil {
types = append(types, EventTypeConnectionState)
}
if e.PresetUpdated != nil {
types = append(types, EventTypePresetUpdated)
}
if e.ZoneUpdated != nil {
types = append(types, EventTypeZoneUpdated)
}
if e.BassUpdated != nil {
types = append(types, EventTypeBassUpdated)
}
if e.ClockTimeUpdated != nil {
types = append(types, EventTypeClockTimeUpdated)
}
if e.ClockDisplayUpdated != nil {
types = append(types, EventTypeClockDisplayUpdated)
}
if e.NameUpdated != nil {
types = append(types, EventTypeNameUpdated)
}
if e.ErrorUpdated != nil {
types = append(types, EventTypeErrorUpdated)
}
if e.RecentsUpdated != nil {
types = append(types, EventTypeRecentsUpdated)
}
if e.LanguageUpdated != nil {
types = append(types, EventTypeLanguageUpdated)
}
return types
}
// String returns a human-readable string representation of the WebSocket event
func (e *WebSocketEvent) String() string {
events := e.GetEvents()
eventTypes := e.GetEventTypes()
if len(events) == 0 {
return fmt.Sprintf("WebSocket Event [Device: %s] - No events", e.DeviceID)
}
if len(events) == 1 {
return fmt.Sprintf("WebSocket Event [Device: %s] - %s", e.DeviceID, eventTypes[0].String())
}
return fmt.Sprintf("WebSocket Event [Device: %s] - %d events", e.DeviceID, len(events))
}