diff --git a/cmd/mdns-scanner/main.go b/cmd/mdns-scanner/main.go
index 1995429..08a0d80 100644
--- a/cmd/mdns-scanner/main.go
+++ b/cmd/mdns-scanner/main.go
@@ -14,6 +14,72 @@ import (
"github.com/hashicorp/mdns"
)
+func displayResults(services []ServiceInfo) {
+ if len(services) == 0 {
+ fmt.Println("No services found.")
+ fmt.Println()
+ fmt.Println("This could mean:")
+ fmt.Println("- No mDNS services on network")
+ fmt.Println("- Network blocks multicast traffic")
+ fmt.Println("- Firewall blocks mDNS port 5353")
+ fmt.Println("- Try different service types or increase timeout")
+
+ return
+ }
+
+ // Group services by type for better display
+ serviceGroups := make(map[string][]ServiceInfo)
+ for _, s := range services {
+ serviceGroups[s.ServiceType] = append(serviceGroups[s.ServiceType], s)
+ }
+
+ // Display grouped services
+ for serviceType, serviceList := range serviceGroups {
+ fmt.Printf("Service Type: %s\n", serviceType)
+ fmt.Printf(" Found %d instance(s):\n", len(serviceList))
+
+ for i, s := range serviceList {
+ fmt.Printf(" %d. %s\n", i+1, s.Name)
+
+ if s.Host != "" {
+ fmt.Printf(" Host: %s\n", s.Host)
+ }
+
+ if s.IPv4 != "" {
+ fmt.Printf(" IPv4: %s\n", s.IPv4)
+ }
+
+ if s.IPv6 != "" {
+ fmt.Printf(" IPv6: %s\n", s.IPv6)
+ }
+
+ if s.Port > 0 {
+ fmt.Printf(" Port: %d\n", s.Port)
+ }
+
+ if len(s.TxtRecords) > 0 {
+ fmt.Printf(" TXT Records: %v\n", s.TxtRecords)
+ }
+ }
+
+ fmt.Println()
+ }
+}
+
+func showSuggestions(service string) {
+ if service == "_services._dns-sd._udp" {
+ fmt.Println("Common services to look for SoundTouch devices:")
+ fmt.Println("- _soundtouch._tcp.local.")
+ fmt.Println("- _http._tcp.local.")
+ fmt.Println("- _upnp._tcp.local.")
+ fmt.Println("- _device-info._tcp.local.")
+ fmt.Println()
+ fmt.Println("Try scanning specific services:")
+ fmt.Println(" ./mdns-scanner -service _soundtouch._tcp -v")
+ fmt.Println(" ./mdns-scanner -service _http._tcp -v")
+ }
+}
+
func main() {
verbose := flag.Bool("verbose", false, "Enable verbose logging")
timeout := flag.Duration("timeout", 10*time.Second, "Discovery timeout")
@@ -107,68 +173,10 @@ done:
})
// Display results
- if len(services) == 0 {
- fmt.Println("No services found.")
- fmt.Println()
- fmt.Println("This could mean:")
- fmt.Println("- No mDNS services on network")
- fmt.Println("- Network blocks multicast traffic")
- fmt.Println("- Firewall blocks mDNS port 5353")
- fmt.Println("- Try different service types or increase timeout")
- } else {
- // Group services by type for better display
- serviceGroups := make(map[string][]ServiceInfo)
-
- for _, service := range services {
- serviceType := service.ServiceType
- serviceGroups[serviceType] = append(serviceGroups[serviceType], service)
- }
-
- // Display grouped services
- for serviceType, serviceList := range serviceGroups {
- fmt.Printf("Service Type: %s\n", serviceType)
- fmt.Printf(" Found %d instance(s):\n", len(serviceList))
-
- for i, service := range serviceList {
- fmt.Printf(" %d. %s\n", i+1, service.Name)
-
- if service.Host != "" {
- fmt.Printf(" Host: %s\n", service.Host)
- }
-
- if service.IPv4 != "" {
- fmt.Printf(" IPv4: %s\n", service.IPv4)
- }
-
- if service.IPv6 != "" {
- fmt.Printf(" IPv6: %s\n", service.IPv6)
- }
-
- if service.Port > 0 {
- fmt.Printf(" Port: %d\n", service.Port)
- }
-
- if len(service.TxtRecords) > 0 {
- fmt.Printf(" TXT Records: %v\n", service.TxtRecords)
- }
- }
-
- fmt.Println()
- }
- }
+ displayResults(services)
// Show suggestions for common SoundTouch-related services
- if *service == "_services._dns-sd._udp" {
- fmt.Println("Common services to look for SoundTouch devices:")
- fmt.Println("- _soundtouch._tcp.local.")
- fmt.Println("- _http._tcp.local.")
- fmt.Println("- _upnp._tcp.local.")
- fmt.Println("- _device-info._tcp.local.")
- fmt.Println()
- fmt.Println("Try scanning specific services:")
- fmt.Println(" ./mdns-scanner -service _soundtouch._tcp -v")
- fmt.Println(" ./mdns-scanner -service _http._tcp -v")
- }
+ showSuggestions(*service)
}
type ServiceInfo struct {
diff --git a/cmd/soundtouch-cli/cmd_network.go b/cmd/soundtouch-cli/cmd_network.go
index d2b4178..cf9ae01 100644
--- a/cmd/soundtouch-cli/cmd_network.go
+++ b/cmd/soundtouch-cli/cmd_network.go
@@ -3,9 +3,47 @@ package main
import (
"fmt"
+ "github.com/gesellix/bose-soundtouch/pkg/models"
"github.com/urfave/cli/v2"
)
+func printNetworkInterface(i int, iface *models.NetworkInterface) {
+ fmt.Printf("\n Interface %d:\n", i+1)
+ fmt.Printf(" Type: %s\n", iface.GetType())
+
+ if iface.GetName() != "" {
+ fmt.Printf(" Name: %s\n", iface.GetName())
+ }
+
+ if iface.GetIPAddress() != "" {
+ fmt.Printf(" IP Address: %s\n", iface.GetIPAddress())
+ }
+
+ if iface.GetMacAddress() != "" {
+ fmt.Printf(" MAC Address: %s\n", iface.GetMacAddress())
+ }
+
+ fmt.Printf(" State: %s\n", iface.GetStateDescription())
+
+ if iface.IsWiFi() {
+ if iface.GetSSID() != "" {
+ fmt.Printf(" SSID: %s\n", iface.GetSSID())
+ }
+
+ if iface.GetSignal() != "" {
+ fmt.Printf(" Signal: %s (%d%%)\n", iface.GetSignalDescription(), iface.GetSignalQuality())
+ }
+
+ if iface.GetFrequencyKHz() > 0 {
+ fmt.Printf(" Frequency: %s (%s)\n", iface.FormatFrequency(), iface.GetFrequencyBand())
+ }
+
+ if iface.GetMode() != "" {
+ fmt.Printf(" Mode: %s\n", iface.GetModeDescription())
+ }
+ }
+}
+
// getNetworkInfo retrieves network information from the device
func getNetworkInfo(c *cli.Context) error {
clientConfig := GetClientConfig(c)
@@ -38,41 +76,7 @@ func getNetworkInfo(c *cli.Context) error {
fmt.Printf(" Interfaces (%d):\n", len(interfaces))
for i := range interfaces {
- iface := &interfaces[i]
- fmt.Printf("\n Interface %d:\n", i+1)
- fmt.Printf(" Type: %s\n", iface.GetType())
-
- if iface.GetName() != "" {
- fmt.Printf(" Name: %s\n", iface.GetName())
- }
-
- if iface.GetIPAddress() != "" {
- fmt.Printf(" IP Address: %s\n", iface.GetIPAddress())
- }
-
- if iface.GetMacAddress() != "" {
- fmt.Printf(" MAC Address: %s\n", iface.GetMacAddress())
- }
-
- fmt.Printf(" State: %s\n", iface.GetStateDescription())
-
- if iface.IsWiFi() {
- if iface.GetSSID() != "" {
- fmt.Printf(" SSID: %s\n", iface.GetSSID())
- }
-
- if iface.GetSignal() != "" {
- fmt.Printf(" Signal: %s (%d%%)\n", iface.GetSignalDescription(), iface.GetSignalQuality())
- }
-
- if iface.GetFrequencyKHz() > 0 {
- fmt.Printf(" Frequency: %s (%s)\n", iface.FormatFrequency(), iface.GetFrequencyBand())
- }
-
- if iface.GetMode() != "" {
- fmt.Printf(" Mode: %s\n", iface.GetModeDescription())
- }
- }
+ printNetworkInterface(i, &interfaces[i])
}
// Show active connections summary
diff --git a/cmd/soundtouch-cli/cmd_source.go b/cmd/soundtouch-cli/cmd_source.go
index b5e7181..bd3c1cd 100644
--- a/cmd/soundtouch-cli/cmd_source.go
+++ b/cmd/soundtouch-cli/cmd_source.go
@@ -4,9 +4,30 @@ import (
"fmt"
"strings"
+ "github.com/gesellix/bose-soundtouch/pkg/models"
"github.com/urfave/cli/v2"
)
+func printSource(source models.SourceItem) {
+ fmt.Printf(" ⢠%s", source.GetDisplayName())
+
+ if source.SourceAccount != "" && source.SourceAccount != source.Source {
+ fmt.Printf(" (%s)", source.SourceAccount)
+ }
+
+ var attributes []string
+ if source.IsLocalSource() {
+ attributes = append(attributes, "Local")
+ attributes = append(attributes, "Available")
+ }
+
+ if len(attributes) > 0 {
+ fmt.Printf(" [%s]", strings.Join(attributes, ", "))
+ }
+
+ fmt.Println()
+}
+
// listSources handles listing available audio sources
func listSources(c *cli.Context) error {
clientConfig := GetClientConfig(c)
@@ -32,26 +53,7 @@ func listSources(c *cli.Context) error {
fmt.Printf(" Ready Sources:\n")
for _, source := range availableSources {
- fmt.Printf(" ⢠%s", source.GetDisplayName())
-
- if source.SourceAccount != "" && source.SourceAccount != source.Source {
- fmt.Printf(" (%s)", source.SourceAccount)
- }
-
- var attributes []string
- if source.IsLocalSource() {
- attributes = append(attributes, "Local")
- }
-
- if source.IsLocalSource() {
- attributes = append(attributes, "Available")
- }
-
- if len(attributes) > 0 {
- fmt.Printf(" [%s]", strings.Join(attributes, ", "))
- }
-
- fmt.Println()
+ printSource(source)
}
}
diff --git a/cmd/websocket-demo/main.go b/cmd/websocket-demo/main.go
index c5c7ed4..e1a7bc5 100644
--- a/cmd/websocket-demo/main.go
+++ b/cmd/websocket-demo/main.go
@@ -43,6 +43,88 @@ func parseHostPort(hostPort string, defaultPort int) (string, int) {
return hostPort, defaultPort
}
+func parseFilters(eventFilter string) map[string]bool {
+ validFilters := map[string]bool{
+ "nowPlaying": true, "volume": true, "connection": true,
+ "preset": true, "zone": true, "bass": true,
+ }
+
+ if eventFilter == "" {
+ return nil
+ }
+
+ filters := make(map[string]bool)
+ filterList := strings.Split(eventFilter, ",")
+
+ for _, f := range filterList {
+ f = strings.TrimSpace(f)
+ if !validFilters[f] {
+ fmt.Printf("Invalid filter '%s'. Valid filters: nowPlaying, volume, connection, preset, zone, bass\n", f)
+ os.Exit(1)
+ }
+
+ filters[f] = true
+ }
+
+ return filters
+}
+
+func discoverDevice(discoverFlag bool, hostPort string, defaultPort int) (string, int, error) {
+ if hostPort != "" && !discoverFlag {
+ deviceHost, devicePort := parseHostPort(hostPort, defaultPort)
+ fmt.Printf("Connecting to: %s:%d\n", deviceHost, devicePort)
+
+ return deviceHost, devicePort, nil
+ }
+
+ fmt.Println("Discovering SoundTouch devices...")
+
+ ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+ defer cancel()
+
+ cfg := &config.Config{
+ DiscoveryTimeout: 10 * time.Second,
+ CacheEnabled: false,
+ }
+ discoveryService := discovery.NewUnifiedDiscoveryService(cfg)
+
+ devices, err := discoveryService.DiscoverDevices(ctx)
+ if err != nil || len(devices) == 0 {
+ if err != nil {
+ return "", 0, fmt.Errorf("discovery failed: %w", err)
+ }
+
+ return "", 0, fmt.Errorf("no SoundTouch devices found")
+ }
+
+ device := devices[0]
+ fmt.Printf("Found %d device(s), connecting to: %s (%s:%d)\n",
+ len(devices), device.Name, device.Host, device.Port)
+
+ return device.Host, device.Port, nil
+}
+
+func setupWebSocket(soundTouchClient *client.Client, reconnect, verbose bool) *client.WebSocketClient {
+ wsConfig := &client.WebSocketConfig{
+ ReconnectInterval: 5 * time.Second,
+ MaxReconnectAttempts: 0, // Unlimited if reconnect enabled
+ PingInterval: 30 * time.Second,
+ PongTimeout: 10 * time.Second,
+ ReadBufferSize: 2048,
+ WriteBufferSize: 2048,
+ }
+
+ if verbose {
+ wsConfig.Logger = &VerboseLogger{}
+ }
+
+ if !reconnect {
+ wsConfig.MaxReconnectAttempts = 1
+ }
+
+ return soundTouchClient.NewWebSocketClient(wsConfig)
+}
+
func main() {
var (
host = flag.String("host", "", "SoundTouch device host/IP address (can include port like host:8090)")
@@ -64,69 +146,13 @@ func main() {
}
// Validate filter if provided
- validFilters := map[string]bool{
- "nowPlaying": true, "volume": true, "connection": true,
- "preset": true, "zone": true, "bass": true,
- }
-
- var filters map[string]bool
- if *eventFilter != "" {
- filters = make(map[string]bool)
-
- filterList := strings.Split(*eventFilter, ",")
- for _, f := range filterList {
- f = strings.TrimSpace(f)
- if !validFilters[f] {
- fmt.Printf("Invalid filter '%s'. Valid filters: nowPlaying, volume, connection, preset, zone, bass\n", f)
- os.Exit(1)
- }
-
- filters[f] = true
- }
- }
-
- var (
- deviceHost string
- devicePort int
- )
+ filters := parseFilters(*eventFilter)
// Discover devices if no host specified or discover flag used
-
- if *host == "" || *discover {
- fmt.Println("Discovering SoundTouch devices...")
-
- ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
- defer cancel()
-
- // Create unified discovery service
- cfg := &config.Config{
- DiscoveryTimeout: 10 * time.Second,
- CacheEnabled: false,
- }
- discoveryService := discovery.NewUnifiedDiscoveryService(cfg)
-
- devices, err := discoveryService.DiscoverDevices(ctx)
- if err != nil {
- fmt.Printf("Discovery failed: %v\n", err)
- return
- }
-
- if len(devices) == 0 {
- fmt.Println("No SoundTouch devices found")
- return
- }
-
- // Use first discovered device
- device := devices[0]
- deviceHost = device.Host
- devicePort = device.Port
-
- fmt.Printf("Found %d device(s), connecting to: %s (%s:%d)\n",
- len(devices), device.Name, device.Host, device.Port)
- } else {
- // Parse provided host
- deviceHost, devicePort = parseHostPort(*host, *port)
- fmt.Printf("Connecting to: %s:%d\n", deviceHost, devicePort)
+ deviceHost, devicePort, err := discoverDevice(*discover, *host, *port)
+ if err != nil {
+ fmt.Printf("Error: %v\n", err)
+ return
}
// Create client
@@ -156,24 +182,7 @@ func main() {
deviceInfo.Name, deviceInfo.Type, macAddress)
// Create WebSocket client
- wsConfig := &client.WebSocketConfig{
- ReconnectInterval: 5 * time.Second,
- MaxReconnectAttempts: 0, // Unlimited if reconnect enabled
- PingInterval: 30 * time.Second,
- PongTimeout: 10 * time.Second,
- ReadBufferSize: 2048,
- WriteBufferSize: 2048,
- }
-
- if *verbose {
- wsConfig.Logger = &VerboseLogger{}
- }
-
- if !*reconnect {
- wsConfig.MaxReconnectAttempts = 1
- }
-
- wsClient := soundTouchClient.NewWebSocketClient(wsConfig)
+ wsClient := setupWebSocket(soundTouchClient, *reconnect, *verbose)
// Set up event handlers
setupEventHandlers(wsClient, filters, *verbose)
@@ -181,9 +190,10 @@ func main() {
// Connect to WebSocket
fmt.Println("Connecting to WebSocket...")
- err = wsClient.ConnectWithConfig(wsConfig)
+ err = wsClient.Connect()
if err != nil {
fmt.Printf("Failed to connect to WebSocket: %v\n", err)
+
return
}
@@ -243,150 +253,175 @@ func main() {
fmt.Println("Disconnected successfully")
}
+func handleNowPlaying(event *models.NowPlayingUpdatedEvent, verbose bool) {
+ fmt.Printf("\nšµ Now Playing Update [%s]:\n", event.DeviceID)
+ np := &event.NowPlaying
+
+ if np.IsEmpty() {
+ fmt.Println(" ā¹ļø Nothing playing")
+ return
+ }
+
+ fmt.Printf(" šµ %s\n", np.GetDisplayTitle())
+
+ if artist := np.GetDisplayArtist(); artist != "" {
+ fmt.Printf(" š¤ %s\n", artist)
+ }
+
+ if np.Album != "" {
+ fmt.Printf(" šæ %s\n", np.Album)
+ }
+
+ fmt.Printf(" š» Source: %s\n", np.Source)
+ fmt.Printf(" ā¶ļø Status: %s\n", np.PlayStatus.String())
+
+ if np.HasTimeInfo() {
+ fmt.Printf(" ā±ļø Duration: %s\n", np.FormatDuration())
+ }
+
+ if np.ShuffleSetting != "" {
+ fmt.Printf(" š Shuffle: %s\n", np.ShuffleSetting.String())
+ }
+
+ if np.RepeatSetting != "" {
+ fmt.Printf(" š Repeat: %s\n", np.RepeatSetting.String())
+ }
+
+ if verbose {
+ fmt.Printf(" š± Raw Source: %s, Account: %s\n", np.Source, np.SourceAccount)
+
+ if np.Art != nil && np.Art.URL != "" {
+ fmt.Printf(" š¼ļø Artwork: %s\n", np.Art.URL)
+ }
+ }
+}
+
+func handleVolume(event *models.VolumeUpdatedEvent, verbose bool) {
+ vol := &event.Volume
+ fmt.Printf("\nš Volume Update [%s]:\n", event.DeviceID)
+
+ if vol.IsMuted() {
+ fmt.Println(" š Muted")
+ } else {
+ fmt.Printf(" š Level: %d\n", vol.ActualVolume)
+
+ if vol.TargetVolume != vol.ActualVolume {
+ fmt.Printf(" šÆ Target: %d\n", vol.TargetVolume)
+ }
+
+ fmt.Printf(" š %s\n", models.GetVolumeLevelName(vol.ActualVolume))
+ }
+
+ if verbose {
+ fmt.Printf(" š± Sync: %v\n", vol.IsVolumeSync())
+ }
+}
+
+func handleConnection(event *models.ConnectionStateUpdatedEvent) {
+ cs := &event.ConnectionState
+ fmt.Printf("\nš Connection Update [%s]:\n", event.DeviceID)
+
+ if cs.IsConnected() {
+ fmt.Println(" ā
Connected")
+ } else {
+ fmt.Printf(" ā State: %s\n", cs.State)
+ }
+
+ if cs.Signal != "" {
+ fmt.Printf(" š¶ Signal: %s\n", cs.GetSignalStrength())
+ }
+}
+
+func handlePreset(event *models.PresetUpdatedEvent, verbose bool) {
+ preset := &event.Preset
+ fmt.Printf("\nš» Preset Update [%s]:\n", event.DeviceID)
+ fmt.Printf(" š» Preset: %d\n", preset.ID)
+
+ if preset.ContentItem != nil {
+ fmt.Printf(" šµ %s\n", preset.ContentItem.ItemName)
+ fmt.Printf(" š» Source: %s\n", preset.ContentItem.Source)
+ }
+
+ if verbose {
+ fmt.Printf(" š± Raw preset data: ID=%d\n", preset.ID)
+ }
+}
+
+func handleZone(event *models.ZoneUpdatedEvent) {
+ zone := &event.Zone
+ fmt.Printf("\nš Zone Update [%s]:\n", event.DeviceID)
+ fmt.Printf(" š Master: %s\n", zone.Master)
+
+ if len(zone.Members) > 0 {
+ fmt.Printf(" š„ Members (%d):\n", len(zone.Members))
+
+ for i, member := range zone.Members {
+ fmt.Printf(" %d. %s (%s)\n", i+1, member.DeviceID, member.IP)
+ }
+ } else {
+ fmt.Println(" š¤ Single device (no zone)")
+ }
+}
+
+func handleBass(event *models.BassUpdatedEvent) {
+ bass := &event.Bass
+ fmt.Printf("\nšµ Bass Update [%s]:\n", event.DeviceID)
+ fmt.Printf(" šļø Level: %d\n", bass.ActualBass)
+
+ if bass.TargetBass != bass.ActualBass {
+ fmt.Printf(" šÆ Target: %d\n", bass.TargetBass)
+ }
+
+ levelDesc := "Neutral"
+ if bass.ActualBass > 0 {
+ levelDesc = "Boosted"
+ } else if bass.ActualBass < 0 {
+ levelDesc = "Reduced"
+ }
+
+ fmt.Printf(" š %s\n", levelDesc)
+}
+
func setupEventHandlers(wsClient *client.WebSocketClient, filters map[string]bool, verbose bool) {
// Now Playing events
if filters == nil || filters["nowPlaying"] {
wsClient.OnNowPlaying(func(event *models.NowPlayingUpdatedEvent) {
- fmt.Printf("\nšµ Now Playing Update [%s]:\n", event.DeviceID)
- np := &event.NowPlaying
-
- if np.IsEmpty() {
- fmt.Println(" ā¹ļø Nothing playing")
- } else {
- fmt.Printf(" šµ %s\n", np.GetDisplayTitle())
-
- if artist := np.GetDisplayArtist(); artist != "" {
- fmt.Printf(" š¤ %s\n", artist)
- }
-
- if np.Album != "" {
- fmt.Printf(" šæ %s\n", np.Album)
- }
-
- fmt.Printf(" š» Source: %s\n", np.Source)
- fmt.Printf(" ā¶ļø Status: %s\n", np.PlayStatus.String())
-
- if np.HasTimeInfo() {
- fmt.Printf(" ā±ļø Duration: %s\n", np.FormatDuration())
- }
-
- if np.ShuffleSetting != "" {
- fmt.Printf(" š Shuffle: %s\n", np.ShuffleSetting.String())
- }
-
- if np.RepeatSetting != "" {
- fmt.Printf(" š Repeat: %s\n", np.RepeatSetting.String())
- }
- }
-
- if verbose {
- fmt.Printf(" š± Raw Source: %s, Account: %s\n", np.Source, np.SourceAccount)
-
- if np.Art != nil && np.Art.URL != "" {
- fmt.Printf(" š¼ļø Artwork: %s\n", np.Art.URL)
- }
- }
+ handleNowPlaying(event, verbose)
})
}
// Volume events
if filters == nil || filters["volume"] {
wsClient.OnVolumeUpdated(func(event *models.VolumeUpdatedEvent) {
- vol := &event.Volume
- fmt.Printf("\nš Volume Update [%s]:\n", event.DeviceID)
-
- if vol.IsMuted() {
- fmt.Println(" š Muted")
- } else {
- fmt.Printf(" š Level: %d\n", vol.ActualVolume)
-
- if vol.TargetVolume != vol.ActualVolume {
- fmt.Printf(" šÆ Target: %d\n", vol.TargetVolume)
- }
-
- fmt.Printf(" š %s\n", models.GetVolumeLevelName(vol.ActualVolume))
- }
-
- if verbose {
- fmt.Printf(" š± Sync: %v\n", vol.IsVolumeSync())
- }
+ handleVolume(event, verbose)
})
}
// Connection state events
if filters == nil || filters["connection"] {
wsClient.OnConnectionState(func(event *models.ConnectionStateUpdatedEvent) {
- cs := &event.ConnectionState
- fmt.Printf("\nš Connection Update [%s]:\n", event.DeviceID)
-
- if cs.IsConnected() {
- fmt.Println(" ā
Connected")
- } else {
- fmt.Printf(" ā State: %s\n", cs.State)
- }
-
- if cs.Signal != "" {
- fmt.Printf(" š¶ Signal: %s\n", cs.GetSignalStrength())
- }
+ handleConnection(event)
})
}
// Preset events
if filters == nil || filters["preset"] {
wsClient.OnPresetUpdated(func(event *models.PresetUpdatedEvent) {
- preset := &event.Preset
- fmt.Printf("\nš» Preset Update [%s]:\n", event.DeviceID)
- fmt.Printf(" š» Preset: %d\n", preset.ID)
-
- if preset.ContentItem != nil {
- fmt.Printf(" šµ %s\n", preset.ContentItem.ItemName)
- fmt.Printf(" š» Source: %s\n", preset.ContentItem.Source)
- }
-
- if verbose {
- fmt.Printf(" š± Raw preset data: ID=%d\n", preset.ID)
- }
+ handlePreset(event, verbose)
})
}
// Zone/Multiroom events
if filters == nil || filters["zone"] {
wsClient.OnZoneUpdated(func(event *models.ZoneUpdatedEvent) {
- zone := &event.Zone
- fmt.Printf("\nš Zone Update [%s]:\n", event.DeviceID)
- fmt.Printf(" š Master: %s\n", zone.Master)
-
- if len(zone.Members) > 0 {
- fmt.Printf(" š„ Members (%d):\n", len(zone.Members))
-
- for i, member := range zone.Members {
- fmt.Printf(" %d. %s (%s)\n", i+1, member.DeviceID, member.IP)
- }
- } else {
- fmt.Println(" š¤ Single device (no zone)")
- }
+ handleZone(event)
})
}
// Bass events
if filters == nil || filters["bass"] {
wsClient.OnBassUpdated(func(event *models.BassUpdatedEvent) {
- bass := &event.Bass
- fmt.Printf("\nšµ Bass Update [%s]:\n", event.DeviceID)
- fmt.Printf(" šļø Level: %d\n", bass.ActualBass)
-
- if bass.TargetBass != bass.ActualBass {
- fmt.Printf(" šÆ Target: %d\n", bass.TargetBass)
- }
-
- levelDesc := "Neutral"
- if bass.ActualBass > 0 {
- levelDesc = "Boosted"
- } else if bass.ActualBass < 0 {
- levelDesc = "Reduced"
- }
-
- fmt.Printf(" š %s\n", levelDesc)
+ handleBass(event)
})
}
diff --git a/pkg/client/websocket.go b/pkg/client/websocket.go
index b5340ed..b7c3656 100644
--- a/pkg/client/websocket.go
+++ b/pkg/client/websocket.go
@@ -368,6 +368,68 @@ func (ws *WebSocketClient) handleMessage(data []byte) {
ws.handleEvent(event)
}
+func (ws *WebSocketClient) dispatchTypedEvent(handlers *models.WebSocketEventHandlers, eventType models.WebSocketEventType, event *models.WebSocketEvent) bool {
+ switch eventType {
+ case models.EventTypeNowPlaying:
+ if handlers.OnNowPlaying != nil && event.NowPlayingUpdated != nil {
+ handlers.OnNowPlaying(event.NowPlayingUpdated)
+ }
+
+ return true
+
+ case models.EventTypeVolumeUpdated:
+ if handlers.OnVolumeUpdated != nil && event.VolumeUpdated != nil {
+ handlers.OnVolumeUpdated(event.VolumeUpdated)
+ }
+
+ return true
+
+ case models.EventTypeConnectionState:
+ if handlers.OnConnectionState != nil && event.ConnectionStateUpdated != nil {
+ handlers.OnConnectionState(event.ConnectionStateUpdated)
+ }
+
+ return true
+
+ case models.EventTypePresetUpdated:
+ if handlers.OnPresetUpdated != nil && event.PresetUpdated != nil {
+ handlers.OnPresetUpdated(event.PresetUpdated)
+ }
+
+ return true
+
+ default:
+ return ws.dispatchTypedEventContinued(handlers, eventType, event)
+ }
+}
+
+func (ws *WebSocketClient) dispatchTypedEventContinued(handlers *models.WebSocketEventHandlers, eventType models.WebSocketEventType, event *models.WebSocketEvent) bool {
+ switch eventType {
+ case models.EventTypeZoneUpdated:
+ if handlers.OnZoneUpdated != nil && event.ZoneUpdated != nil {
+ handlers.OnZoneUpdated(event.ZoneUpdated)
+ }
+
+ return true
+
+ case models.EventTypeBassUpdated:
+ if handlers.OnBassUpdated != nil && event.BassUpdated != nil {
+ handlers.OnBassUpdated(event.BassUpdated)
+ }
+
+ return true
+
+ case models.EventTypeRecentsUpdated:
+ return true
+
+ case models.EventTypeLanguageUpdated:
+ return true
+
+ default:
+ return false
+ }
+}
+
// handleEvent dispatches events to appropriate handlers
func (ws *WebSocketClient) handleEvent(event *models.WebSocketEvent) {
ws.mu.RLock()
@@ -378,41 +440,8 @@ func (ws *WebSocketClient) handleEvent(event *models.WebSocketEvent) {
hasKnownEvent := false
for _, eventType := range eventTypes {
- hasKnownEvent = true
-
- switch eventType {
- case models.EventTypeNowPlaying:
- if handlers.OnNowPlaying != nil && event.NowPlayingUpdated != nil {
- handlers.OnNowPlaying(event.NowPlayingUpdated)
- }
-
- case models.EventTypeVolumeUpdated:
- if handlers.OnVolumeUpdated != nil && event.VolumeUpdated != nil {
- handlers.OnVolumeUpdated(event.VolumeUpdated)
- }
-
- case models.EventTypeConnectionState:
- if handlers.OnConnectionState != nil && event.ConnectionStateUpdated != nil {
- handlers.OnConnectionState(event.ConnectionStateUpdated)
- }
-
- case models.EventTypePresetUpdated:
- if handlers.OnPresetUpdated != nil && event.PresetUpdated != nil {
- handlers.OnPresetUpdated(event.PresetUpdated)
- }
-
- case models.EventTypeZoneUpdated:
- if handlers.OnZoneUpdated != nil && event.ZoneUpdated != nil {
- handlers.OnZoneUpdated(event.ZoneUpdated)
- }
-
- case models.EventTypeBassUpdated:
- if handlers.OnBassUpdated != nil && event.BassUpdated != nil {
- handlers.OnBassUpdated(event.BassUpdated)
- }
-
- default:
- hasKnownEvent = false
+ if ws.dispatchTypedEvent(handlers, eventType, event) {
+ hasKnownEvent = true
}
}
diff --git a/pkg/models/balance_marshal_test.go b/pkg/models/balance_marshal_test.go
index a469965..6eaef83 100644
--- a/pkg/models/balance_marshal_test.go
+++ b/pkg/models/balance_marshal_test.go
@@ -22,7 +22,9 @@ func TestBalanceMarshalXML(t *testing.T) {
t.Fatalf("MarshalXML failed: %v", err)
}
- encoder.Flush()
+ if err := encoder.Flush(); err != nil {
+ t.Fatalf("Flush failed: %v", err)
+ }
// Convert to string for easier testing
xmlStr := buf.String()
@@ -57,10 +59,11 @@ func TestBalanceMarshalXML_PositiveValue(t *testing.T) {
t.Fatalf("MarshalXML failed: %v", err)
}
- encoder.Flush()
+ if err := encoder.Flush(); err != nil {
+ t.Fatalf("Flush failed: %v", err)
+ }
xmlStr := buf.String()
-
expectedElements := []string{
`deviceID="ABCDEF123456"`,
`30`,
@@ -90,10 +93,11 @@ func TestBalanceMarshalXML_ZeroValue(t *testing.T) {
t.Fatalf("MarshalXML failed: %v", err)
}
- encoder.Flush()
+ if err := encoder.Flush(); err != nil {
+ t.Fatalf("Flush failed: %v", err)
+ }
xmlStr := buf.String()
-
expectedElements := []string{
`deviceID="ZERO0000TEST"`,
`0`,
@@ -123,10 +127,11 @@ func TestBalanceMarshalXML_ExtremeValues(t *testing.T) {
t.Fatalf("MarshalXML failed: %v", err)
}
- encoder.Flush()
+ if err := encoder.Flush(); err != nil {
+ t.Fatalf("Flush failed: %v", err)
+ }
xmlStr := buf.String()
-
expectedElements := []string{
`deviceID="EXTREME_TEST"`,
`-50`,
diff --git a/pkg/models/bass_utilities_test.go b/pkg/models/bass_utilities_test.go
index 9dbd493..49c3a20 100644
--- a/pkg/models/bass_utilities_test.go
+++ b/pkg/models/bass_utilities_test.go
@@ -230,9 +230,10 @@ func TestBassMarshalXML(t *testing.T) {
t.Fatalf("MarshalXML failed: %v", err)
}
- encoder.Flush()
+ if err := encoder.Flush(); err != nil {
+ t.Fatalf("Flush failed: %v", err)
+ }
- // Convert to string for easier testing
xmlStr := buf.String()
// Check that XML contains expected elements
diff --git a/pkg/models/websocket.go b/pkg/models/websocket.go
index 576c1b5..2987742 100644
--- a/pkg/models/websocket.go
+++ b/pkg/models/websocket.go
@@ -334,83 +334,98 @@ func ParseWebSocketEvent(data []byte) (*WebSocketEvent, error) {
return &event, nil
}
+func (e *WebSocketEvent) getFieldByEventType(eventType WebSocketEventType) interface{} {
+ var field interface{}
+
+ switch eventType {
+ case EventTypeNowPlaying:
+ field = e.NowPlayingUpdated
+ case EventTypeVolumeUpdated:
+ field = e.VolumeUpdated
+ case EventTypeConnectionState:
+ field = e.ConnectionStateUpdated
+ case EventTypePresetUpdated:
+ field = e.PresetUpdated
+ case EventTypeZoneUpdated:
+ field = e.ZoneUpdated
+ case EventTypeBassUpdated:
+ field = e.BassUpdated
+ case EventTypeClockTimeUpdated:
+ field = e.ClockTimeUpdated
+ case EventTypeClockDisplayUpdated:
+ field = e.ClockDisplayUpdated
+ case EventTypeNameUpdated:
+ field = e.NameUpdated
+ case EventTypeErrorUpdated:
+ field = e.ErrorUpdated
+ case EventTypeRecentsUpdated:
+ field = e.RecentsUpdated
+ case EventTypeLanguageUpdated:
+ field = e.LanguageUpdated
+ }
+
+ // Use reflection or a type-safe check to ensure we only return non-nil interfaces
+ // In Go, an interface is nil only if both its type and value are nil.
+ // If e.NowPlayingUpdated is a nil pointer, field will be a non-nil interface containing a nil pointer.
+ // We need to return a literal nil if the field is empty to satisfy expectations.
+
+ if field == nil {
+ return nil
+ }
+
+ // We know all these fields are pointers.
+ // We can't easily check for nil pointer without reflection here in a generic way,
+ // but we can restore the previous logic in a more compact way if needed.
+ // Actually, the previous logic was: if e.NowPlayingUpdated != nil { return e.NowPlayingUpdated }
+ // which returns a non-nil interface.
+
+ return field
+}
+
+// isNil checks if an interface is nil or contains a nil pointer.
+func isNil(i interface{}) bool {
+ if i == nil {
+ return true
+ }
+
+ switch v := i.(type) {
+ case *NowPlayingUpdatedEvent:
+ return v == nil
+ case *VolumeUpdatedEvent:
+ return v == nil
+ case *ConnectionStateUpdatedEvent:
+ return v == nil
+ case *PresetUpdatedEvent:
+ return v == nil
+ case *ZoneUpdatedEvent:
+ return v == nil
+ case *BassUpdatedEvent:
+ return v == nil
+ case *ClockTimeUpdatedEvent:
+ return v == nil
+ case *ClockDisplayUpdatedEvent:
+ return v == nil
+ case *NameUpdatedEvent:
+ return v == nil
+ case *ErrorUpdatedEvent:
+ return v == nil
+ case *RecentsUpdatedEvent:
+ return v == nil
+ case *LanguageUpdatedEvent:
+ return v == nil
+ }
+
+ return false
+}
+
// 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
- }
+ field := event.getFieldByEventType(eventType)
+ if !isNil(field) {
+ if typedResult, ok := field.(T); ok {
+ return typedResult, nil
}
}