Bose SoundTouch Toolkit

Documentation for controlling and preserving Bose SoundTouch devices

View the Project on GitHub gesellix/Bose-SoundTouch

WebSocket Events - Real-time SoundTouch Monitoring

This document describes the WebSocket event functionality for real-time monitoring of Bose SoundTouch devices.

Overview

The WebSocket client provides real-time event notifications for various device state changes including:

Quick Start

Basic Usage

package main

import (
    "log"
    "time"
    
    "github.com/gesellix/bose-soundtouch/pkg/client"
    "github.com/gesellix/bose-soundtouch/pkg/models"
)

func main() {
    // Create SoundTouch client
    soundTouchClient := client.NewClientFromHost("192.168.1.10")
    
    // Create WebSocket client with default configuration
    wsClient := soundTouchClient.NewWebSocketClient(nil)
    
    // Set up event handlers
    wsClient.OnNowPlaying(func(event *models.NowPlayingUpdatedEvent) {
        np := &event.NowPlaying
        log.Printf("Now Playing: %s by %s", np.Track, np.Artist)
        log.Printf("Status: %s", np.PlayStatus.String())
    })
    
    wsClient.OnVolumeUpdated(func(event *models.VolumeUpdatedEvent) {
        vol := &event.Volume
        if vol.IsMuted() {
            log.Println("Volume: Muted")
        } else {
            log.Printf("Volume: %d", vol.ActualVolume)
        }
    })
    
    // Connect to WebSocket
    if err := wsClient.Connect(); err != nil {
        log.Fatalf("Failed to connect: %v", err)
    }
    
    // Keep running
    wsClient.Wait()
}

Using the CLI

The recommended way to monitor WebSocket events is through the built-in CLI command:

# Monitor all events from a specific device
soundtouch-cli --host 192.168.1.10 events subscribe

# Monitor only volume and now playing events
soundtouch-cli --host 192.168.1.10 events subscribe --filter volume,nowPlaying

# Monitor for 5 minutes with verbose output
soundtouch-cli --host 192.168.1.10 events subscribe --duration 5m --verbose

# Monitor zone events without automatic reconnection
soundtouch-cli --host 192.168.1.10 events subscribe --filter zone --no-reconnect

Using the CLI Demo (Alternative)

For development or testing purposes, you can also use the standalone demo:

# Auto-discover device and monitor all events
go run ./cmd/websocket-demo -discover

# Connect to specific device
go run ./cmd/websocket-demo -host 192.168.1.10

# Monitor only volume changes for 5 minutes
go run ./cmd/websocket-demo -host 192.168.1.10 -filter volume -duration 5m

# Monitor multiple event types with verbose logging
go run ./cmd/websocket-demo -host 192.168.1.10 -filter nowPlaying,volume -verbose

Configuration

WebSocket Configuration Options

config := &client.WebSocketConfig{
    // Reconnection settings
    ReconnectInterval:    5 * time.Second,  // Time between reconnection attempts
    MaxReconnectAttempts: 0,                // 0 = unlimited attempts
    
    // Keep-alive settings
    PingInterval:         30 * time.Second, // Ping frequency
    PongTimeout:          10 * time.Second, // Pong response timeout
    
    // Buffer sizes
    ReadBufferSize:       2048,             // WebSocket read buffer
    WriteBufferSize:      2048,             // WebSocket write buffer
    
    // Logging
    Logger:               customLogger,      // Custom logger implementation
}

wsClient := soundTouchClient.NewWebSocketClient(config)

Default Configuration

If you pass nil to NewWebSocketClient(), these defaults are used:

Event Types and Handlers

1. Now Playing Events

Triggered when playback state, track, or playback settings change.

wsClient.OnNowPlaying(func(event *models.NowPlayingUpdatedEvent) {
    np := &event.NowPlaying
    
    // Basic info
    fmt.Printf("Track: %s\n", np.Track)
    fmt.Printf("Artist: %s\n", np.Artist)
    fmt.Printf("Album: %s\n", np.Album)
    fmt.Printf("Source: %s\n", np.Source)
    
    // Playback status
    fmt.Printf("Status: %s\n", np.PlayStatus.String())
    fmt.Printf("Is Playing: %t\n", np.PlayStatus.IsPlaying())
    
    // Settings
    fmt.Printf("Shuffle: %s\n", np.ShuffleSetting.String())
    fmt.Printf("Repeat: %s\n", np.RepeatSetting.String())
    
    // Time info (if available)
    if np.HasTimeInfo() {
        fmt.Printf("Duration: %s\n", np.FormatDuration())
        fmt.Printf("Position: %s\n", np.FormatPosition())
    }
    
    // Capabilities
    fmt.Printf("Can Skip: %t\n", np.CanSkip())
    fmt.Printf("Can Seek: %t\n", np.IsSeekSupported())
    fmt.Printf("Can Favorite: %t\n", np.CanFavorite())
})

2. Volume Events

Triggered when volume level or mute status changes.

wsClient.OnVolumeUpdated(func(event *models.VolumeUpdatedEvent) {
    vol := &event.Volume
    
    if vol.IsMuted() {
        fmt.Println("Device is muted")
    } else {
        fmt.Printf("Volume: %d\n", vol.ActualVolume)
        fmt.Printf("Level: %s\n", models.GetVolumeLevelName(vol.ActualVolume))
    }
    
    // Check if volume is still transitioning
    if !vol.IsVolumeSync() {
        fmt.Printf("Target volume: %d\n", vol.TargetVolume)
    }
})

3. Connection State Events

Triggered when network connectivity changes.

wsClient.OnConnectionState(func(event *models.ConnectionStateUpdatedEvent) {
    cs := &event.ConnectionState
    
    if cs.IsConnected() {
        fmt.Println("Device connected to network")
        fmt.Printf("Signal strength: %s\n", cs.GetSignalStrength())
    } else {
        fmt.Printf("Connection state: %s\n", cs.State)
    }
})

4. Preset Events

Triggered when presets are updated or selected. Note: Preset creation via API is officially not supported by SoundTouch - presets can only be created through the official app or device controls.

wsClient.OnPresetUpdated(func(event *models.PresetUpdatedEvent) {
    preset := &event.Preset
    
    fmt.Printf("Preset %s updated\n", preset.ID)
    if preset.ContentItem != nil {
        fmt.Printf("Name: %s\n", preset.ContentItem.ItemName)
        fmt.Printf("Source: %s\n", preset.ContentItem.Source)
    }
})

5. Multiroom Zone Events

Triggered when multiroom configuration changes.

wsClient.OnZoneUpdated(func(event *models.ZoneUpdatedEvent) {
    zone := &event.Zone
    
    fmt.Printf("Zone master: %s\n", zone.Master)
    fmt.Printf("Zone members: %d\n", len(zone.Members))
    
    for i, member := range zone.Members {
        fmt.Printf("  %d. %s (%s)\n", i+1, member.DeviceID, member.IP)
    }
})

6. Bass Level Events

Triggered when bass equalizer settings change.

wsClient.OnBassUpdated(func(event *models.BassUpdatedEvent) {
    bass := &event.Bass
    
    fmt.Printf("Bass level: %d\n", bass.ActualBass)
    
    if bass.ActualBass > 0 {
        fmt.Println("Bass boosted")
    } else if bass.ActualBass < 0 {
        fmt.Println("Bass reduced")
    } else {
        fmt.Println("Bass neutral")
    }
})

7. Unknown Events Handler

Handle any events not explicitly supported:

wsClient.OnUnknownEvent(func(event *models.WebSocketEvent) {
    fmt.Printf("Unknown event from device %s\n", event.DeviceID)
    for _, eventType := range event.GetEventTypes() {
        fmt.Printf("  Event type: %s\n", eventType)
    }
})

Connection Management

Connecting and Disconnecting

// Connect with default configuration
err := wsClient.Connect()

// Connect with custom configuration
config := &client.WebSocketConfig{
    ReconnectInterval: 3 * time.Second,
    PingInterval:      15 * time.Second,
}
err := wsClient.ConnectWithConfig(config)

// Check connection status
if wsClient.IsConnected() {
    fmt.Println("WebSocket connected")
}

// Disconnect
err := wsClient.Disconnect()

Automatic Reconnection

The WebSocket client automatically attempts to reconnect when the connection is lost:

config := &client.WebSocketConfig{
    ReconnectInterval:    5 * time.Second,  // Wait 5 seconds between attempts
    MaxReconnectAttempts: 10,               // Try up to 10 times (0 = unlimited)
}

wsClient := soundTouchClient.NewWebSocketClient(config)

Graceful Shutdown

// Set up signal handling for graceful shutdown
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)

go func() {
    <-sigChan
    fmt.Println("Shutting down...")
    wsClient.Disconnect()
}()

// Wait for shutdown
wsClient.Wait()

Error Handling and Logging

Custom Logger

Implement the Logger interface for custom logging:

type CustomLogger struct{}

func (c *CustomLogger) Printf(format string, v ...interface{}) {
    // Custom logging implementation
    log.Printf("[WS] "+format, v...)
}

config := &client.WebSocketConfig{
    Logger: &CustomLogger{},
}

Silent Logging

To disable logging completely:

type SilentLogger struct{}

func (s *SilentLogger) Printf(format string, v ...interface{}) {
    // Do nothing
}

config := &client.WebSocketConfig{
    Logger: &SilentLogger{},
}

Advanced Usage

Event Filtering

Process only specific event types:

wsClient.OnNowPlaying(func(event *models.NowPlayingUpdatedEvent) {
    // Only handle now playing events
})

// Don't set other handlers - they'll be ignored

Multiple Event Handlers

You can set multiple handlers, but only the last one set will be used:

// First handler - will be replaced
wsClient.OnNowPlaying(func(event *models.NowPlayingUpdatedEvent) {
    fmt.Println("Handler 1")
})

// Second handler - this one will be used
wsClient.OnNowPlaying(func(event *models.NowPlayingUpdatedEvent) {
    fmt.Println("Handler 2")
})

Composite Event Handling

Handle multiple event types in a unified way:

handlers := &models.WebSocketEventHandlers{
    OnNowPlaying: func(event *models.NowPlayingUpdatedEvent) {
        logEvent("NowPlaying", event.DeviceID)
    },
    OnVolumeUpdated: func(event *models.VolumeUpdatedEvent) {
        logEvent("Volume", event.DeviceID)
    },
    OnConnectionState: func(event *models.ConnectionStateUpdatedEvent) {
        logEvent("Connection", event.DeviceID)
    },
}

wsClient.SetHandlers(handlers)

WebSocket Protocol Details

Connection Endpoint

The WebSocket connects to:

Example: ws://192.168.1.10:8080/

Message Format

Events are received as XML messages in this format:

<?xml version="1.0" encoding="UTF-8" ?>
<updates deviceID="689E19B8BB8A">
    <nowPlayingUpdated deviceID="689E19B8BB8A">
        <nowPlaying deviceID="689E19B8BB8A" source="SPOTIFY">
            <track>Song Title</track>
            <artist>Artist Name</artist>
            <album>Album Name</album>
            <playStatus>PLAY_STATE</playStatus>
        </nowPlaying>
    </nowPlayingUpdated>
</updates>

Keep-Alive

The client automatically sends WebSocket ping frames to keep the connection alive. The server responds with pong frames.

Troubleshooting

Common Issues

  1. Connection Refused
    • Ensure the device is on the network and reachable
    • Check that WebSocket port 8080 is not blocked by firewall
    • Verify the device supports WebSocket connections
  2. Frequent Disconnections
    • Check network stability
    • Increase ping interval if network is slow
    • Enable verbose logging to see connection details
  3. Events Not Received
    • Verify event handlers are set before connecting
    • Check if the device actually generates the expected events
    • Enable unknown event handler to see all incoming events

Debugging

Enable verbose logging:

config := &client.WebSocketConfig{
    Logger: &VerboseLogger{},
}

type VerboseLogger struct{}

func (v *VerboseLogger) Printf(format string, args ...interface{}) {
    timestamp := time.Now().Format("15:04:05.000")
    fmt.Printf("[%s] [WebSocket] %s\n", timestamp, fmt.Sprintf(format, args...))
}

Testing

Use the CLI demo to test WebSocket functionality:

# Test with verbose output
go run ./cmd/websocket-demo -host 192.168.1.10 -verbose

# Test reconnection by temporarily disconnecting device
go run ./cmd/websocket-demo -host 192.168.1.10 -verbose -duration 5m

Performance Considerations

Security Notes

API Limitations

Preset Management

The SoundTouch API officially does not support preset creation or modification via WebSocket or HTTP endpoints. Preset events are read-only notifications when presets are updated through:

This is an intentional API design decision to maintain user control over personal preset configurations.

Integration Examples

Home Automation

wsClient.OnNowPlaying(func(event *models.NowPlayingUpdatedEvent) {
    if event.NowPlaying.PlayStatus.IsPlaying() {
        // Dim lights when music starts playing
        homeAutomation.DimLights()
    }
})

wsClient.OnVolumeUpdated(func(event *models.VolumeUpdatedEvent) {
    if event.Volume.ActualVolume > 80 {
        // Send notification for loud volume
        notification.Send("Volume is very loud!")
    }
})

Music Dashboard

type MusicDashboard struct {
    currentTrack string
    volume      int
    isPlaying   bool
}

dashboard := &MusicDashboard{}

wsClient.OnNowPlaying(func(event *models.NowPlayingUpdatedEvent) {
    dashboard.currentTrack = event.NowPlaying.GetDisplayTitle()
    dashboard.isPlaying = event.NowPlaying.PlayStatus.IsPlaying()
    dashboard.updateUI()
})

wsClient.OnVolumeUpdated(func(event *models.VolumeUpdatedEvent) {
    dashboard.volume = event.Volume.ActualVolume
    dashboard.updateUI()
})

API Reference

See the generated Go documentation for complete API details:

go doc github.com/gesellix/bose-soundtouch/pkg/client.WebSocketClient
go doc github.com/gesellix/bose-soundtouch/pkg/models.WebSocketEvent

Testing

Run the WebSocket tests:

# Run unit tests
go test ./pkg/client -v -run TestWebSocket

# Run model tests
go test ./pkg/models -v -run TestWebSocket

# Run benchmarks
go test ./pkg/client -bench=BenchmarkWebSocket