Documentation for controlling and preserving Bose SoundTouch devices
This document describes the WebSocket event functionality for real-time monitoring of Bose SoundTouch devices.
The WebSocket client provides real-time event notifications for various device state changes including:
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()
}
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
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
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)
If you pass nil to NewWebSocketClient(), these defaults are used:
log packageTriggered 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())
})
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)
}
})
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)
}
})
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)
}
})
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)
}
})
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")
}
})
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)
}
})
// 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()
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)
// 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()
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{},
}
To disable logging completely:
type SilentLogger struct{}
func (s *SilentLogger) Printf(format string, v ...interface{}) {
// Do nothing
}
config := &client.WebSocketConfig{
Logger: &SilentLogger{},
}
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
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")
})
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)
The WebSocket connects to:
ws://8080 (different from HTTP API port 8090)/Example: ws://192.168.1.10:8080/
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>
The client automatically sends WebSocket ping frames to keep the connection alive. The server responds with pong frames.
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...))
}
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
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.
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!")
}
})
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()
})
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
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