Documentation for controlling and preserving Bose SoundTouch devices
Complete guide to diagnosing and fixing common SoundTouch Go client issues
This guide helps you quickly identify and resolve problems with the SoundTouch Go client library. Issues are organized by category with step-by-step solutions.
Run these commands to quickly diagnose your setup:
# 1. Test discovery
go run ./cmd/soundtouch-cli -discover
# 2. Test specific device connection
go run ./cmd/soundtouch-cli -host 192.168.1.100 -info
# 3. Test basic controls
go run ./cmd/soundtouch-cli -host 192.168.1.100 -volume
# 4. Test network connectivity
ping 192.168.1.100
Symptoms:
🔍 Discovering SoundTouch devices...
❌ No devices found on the network
Causes & Solutions:
# Check if devices are on same network
ip route show default # Your gateway
arp -a | grep -i bose # Look for Bose devices
Solution: Ensure both your computer and SoundTouch are on the same subnet.
# Check if firewall is blocking UPnP
sudo ufw status # Ubuntu
netsh advfirewall show allprofiles # Windows
Solution: Allow UPnP traffic (port 1900 UDP) or temporarily disable firewall.
discoverer := discovery.NewDiscoverer(discovery.Config{
Timeout: 30 * time.Second, // Increase timeout
})
// Bypass discovery entirely
client := client.NewClientFromHost("192.168.1.100")
Symptoms:
🔍 Discovering SoundTouch devices (timeout: 5s)...
❌ Discovery failed: context deadline exceeded
Solutions:
discoverer := discovery.NewDiscoverer(discovery.Config{
Timeout: 15 * time.Second,
})
ping -c 4 192.168.1.1
iperf3 -c 192.168.1.1 # If iperf server available
3. **Use wired connection if possible**
---
## 🌐 **Connection Issues**
### ❌ "Connection refused"
**Symptoms:**
```go
Failed to connect: dial tcp 192.168.1.100:8090: connection refused
Diagnostic Steps:
# Test if port 8090 is open
telnet 192.168.1.100 8090
# OR
nc -zv 192.168.1.100 8090
# Scan for open ports
nmap -p 8080-8100 192.168.1.100
# Check routing
traceroute 192.168.1.100
# Test basic connectivity
ping -c 4 192.168.1.100
Symptoms:
Failed to get device info: context deadline exceeded
Solutions:
config := client.ClientConfig{
Host: "192.168.1.100",
Port: 8090,
Timeout: 30 * time.Second, // Increase from default 10s
}
# Test response time
ping -c 10 192.168.1.100
# Should be < 100ms typically
Symptoms:
Failed to connect: dial tcp: lookup soundtouch.local: no such host
Solutions:
client := client.NewClientFromHost("192.168.1.100") // Not "soundtouch.local"
nslookup soundtouch.local dig soundtouch.local
sudo apt-get install avahi-utils avahi-resolve -n soundtouch.local
---
## 🎵 **Playback Control Issues**
### ❌ "Play/Pause not working"
**Symptoms:**
- Commands succeed but no audio change
- Device shows wrong status
**Diagnostic Steps:**
#### 1. **Check Current Status**
```go
nowPlaying, err := client.GetNowPlaying()
if err == nil {
fmt.Printf("Status: %s, Source: %s\n",
nowPlaying.PlayStatus, nowPlaying.Source)
}
sources, err := client.GetSources()
if err == nil {
for _, source := range sources.Sources {
fmt.Printf("Source: %s, Status: %s\n",
source.Source, source.Status)
}
}
Solutions:
client.SelectSpotify()
time.Sleep(2 * time.Second) // Wait for source change
client.Play()
client.SendKey("PLAY") // Instead of client.Play()
client.SendKey("PAUSE") // Instead of client.Pause()
Symptoms:
Failed to select source: API request failed with status 500
Solutions:
sources, _ := client.GetSources()
for _, source := range sources.Sources {
if source.Source == "SPOTIFY" && source.Status == "READY" {
// Source is available
client.SelectSource("SPOTIFY", source.SourceAccount)
}
}
// For streaming services, include account
client.SelectSource("SPOTIFY", "your_account_id")
client.SelectSpotify() // Handles account automatically
client.SelectBluetooth()
client.SelectAux()
Symptoms:
Diagnostic Steps:
zoneStatus, err := client.GetZoneStatus()
if err == nil {
fmt.Printf("Zone Status: %s\n", zoneStatus)
}
Solutions:
// Only zone master can control volume
if zoneStatus == "MEMBER" {
fmt.Println("Device is zone member - only master controls volume")
// Find and use master device
zone, _ := client.GetZone()
// Connect to master device using zone.Master ID
}
client.SetVolumeSafe(50) // Clamps to valid range
client.IncreaseVolume(5) // Incremental control
client.DecreaseVolume(5)
volume, _ := client.GetVolume()
fmt.Printf("Target: %d, Actual: %d, Muted: %t\n",
volume.TargetVolume, volume.ActualVolume, volume.Muted)
Symptoms:
Failed to set bass: API request failed with status 404
Solutions:
caps, err := client.GetCapabilities()
if err == nil {
fmt.Printf("Bass capable: %t\n", caps.BassCapable)
}
client.SetBassSafe(-5) // Won't fail on unsupported devices
client.SetBalanceSafe(10) // Falls back gracefully
Symptoms:
$ go run ./cmd/soundtouch-cli --host 192.168.178.35 sp beep
Playing notification beep from 192.168.178.35:8090...
✗ Failed to play notification beep: API request failed with status 400
Cause:
This was a bug in earlier versions where the Go client incorrectly used POST instead of GET for the /playNotification endpoint.
Solution:
Update to the latest version. The fix changed the PlayNotificationBeep() method to use GET requests:
// Fixed implementation (v2025.02+)
func (c *Client) PlayNotificationBeep() error {
var status models.StationResponse
return c.get("/playNotification", &status)
}
Verification: Both commands should now work identically:
# CLI command
go run ./cmd/soundtouch-cli --host 192.168.178.35 sp beep
# Direct curl (for comparison)
curl http://192.168.178.35:8090/playNotification
Symptoms:
✗ Failed to play notification: endpoint not supported
Causes & Solutions:
Solution: Verify device model with:
soundtouch-cli --host <device> info
TTS and URL playback require an app key, but beep does not:
# Beep - no app key needed
soundtouch-cli --host <device> speaker beep
# TTS - app key required
soundtouch-cli --host <device> speaker tts --text "Hello" --app-key "your-key"
Symptoms:
✗ Failed to play notification: device is busy
Solutions:
Only one notification can play at a time. Wait a few seconds and retry.
nowPlaying, _ := client.GetNowPlaying()
fmt.Printf("Current source: %s, status: %s\n",
nowPlaying.Source, nowPlaying.PlayStatus)
Symptoms:
Failed to connect WebSocket: dial ws://192.168.1.100:8080/: connection refused
Solutions:
# WebSocket uses port 8080, not 8090
nc -zv 192.168.1.100 8080
// WebSocket client should auto-handle this
wsClient := client.NewWebSocketClient(nil)
// Manual connection (if needed)
url := "ws://192.168.1.100:8080/"
headers := http.Header{}
headers.Set("Sec-WebSocket-Protocol", "gabbo")
Symptoms:
Solutions:
wsClient := client.NewWebSocketClient(config)
2. **Check network stability:**
```bash
# Test for packet loss
ping -c 100 192.168.1.100 | grep loss
sudo iwconfig wlan0 power off
powercfg -devicequery wake_armed
### ❌ "Events not received"
**Symptoms:**
- WebSocket connects but no events
- Missing volume/playback updates
**Solutions:**
1. **Verify event handlers:**
```go
wsClient.OnVolumeUpdated(func(event *models.VolumeUpdatedEvent) {
fmt.Printf("Volume event received: %d\n", event.Volume.TargetVolume)
})
// Test by manually changing volume on device
wsClient.OnUnknownEvent(func(event *models.WebSocketEvent) {
fmt.Printf("Unknown event: %+v\n", event)
})
Symptoms:
Failed to create zone: API request failed with status 400
Solutions:
// Get device capabilities
caps, _ := client.GetCapabilities()
// Look for multiroom support
// Verify devices are on same network
for _, client := range clients {
network, _ := client.GetNetworkInfo()
fmt.Printf("Device IP: %s\n", network.GetConnectedInterface().IPAddress)
}
// Get exact device IDs
info, _ := client.GetDeviceInfo()
masterID := info.DeviceID // Use this, not MAC address
// Create zone with proper IDs
client.CreateZone(masterID, []string{member1ID, member2ID})
// Don't create multiple zones simultaneously
client1.CreateZone(master1, []string{member1})
time.Sleep(2 * time.Second)
client2.CreateZone(master2, []string{member2})
Symptoms:
Solutions:
if status == “STANDALONE” { // Device didn’t join - check network/permissions }
2. **Firmware compatibility:**
- Ensure all devices have recent firmware
- Update via Bose SoundTouch app
- Some very old devices don't support multiroom
3. **Network subnet issues:**
```bash
# Verify devices can reach each other
ping -c 4 member_device_ip
import "log"
// Enable verbose HTTP logging
log.SetFlags(log.LstdFlags | log.Lshortfile)
// Custom HTTP client with debug
transport := &http.Transport{
// Add debug transport if needed
}
config := client.ClientConfig{
Host: "192.168.1.100",
Port: 8090,
Timeout: 10 * time.Second,
}
wsClient.OnUnknownEvent(func(event *models.WebSocketEvent) {
log.Printf("Raw event: %+v", event)
})
// Enable WebSocket debug logging
config := client.DefaultWebSocketConfig()
config.Logger = &client.DefaultLogger{} // Or custom logger
# Capture SoundTouch traffic
sudo tcpdump -i any host 192.168.1.100 and port 8090
# Monitor WebSocket traffic
sudo tcpdump -i any host 192.168.1.100 and port 8080
# HTTP debugging with curl
curl -v http://192.168.1.100:8090/info
curl -v http://192.168.1.100:8090/volume
Symptoms:
Solutions:
// Use connection pools for multiple devices pool := NewConnectionPool(10, 5*time.Minute) defer pool.Close()
2. **Goroutine leaks:**
```go
// Use context for cancellation
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// Monitor goroutines
go func() {
for {
fmt.Printf("Goroutines: %d\n", runtime.NumGoroutine())
time.Sleep(10 * time.Second)
}
}()
Solutions:
config := client.ClientConfig{
Timeout: 15 * time.Second, // Reasonable for network ops
}
// Reuse connections instead of creating new ones
pool := NewConnectionPool(5, 5*time.Minute)
client := pool.GetClient(host, port)
// Process multiple devices concurrently
var wg sync.WaitGroup
for _, client := range clients {
wg.Add(1)
go func(c *client.Client) {
defer wg.Done()
// Process device
}(client)
}
wg.Wait()
// Cleanup resources defer func() { if wsClient != nil { wsClient.Disconnect() } }()
2. **Resource monitoring:**
```go
// Monitor resource usage
go func() {
var m runtime.MemStats
for {
runtime.ReadMemStats(&m)
log.Printf("Alloc = %d KB, Sys = %d KB", m.Alloc/1024, m.Sys/1024)
time.Sleep(30 * time.Second)
}
}()
Use this checklist to systematically troubleshoot issues:
Symptoms:
GET /streaming/account/3230304/device/A81B6A536A98/presets
→ 500 Internal Server Error
→ Log: "open .../devices/A81B6A536A98/Presets.xml: no such file or directory"
Cause: The service uses MAC addresses in API requests but stores files using device serial numbers. A mapping system resolves MAC addresses to serial numbers automatically.
Quick Solutions:
sudo systemctl restart soundtouch-service
# Files should be stored by serial number, not MAC
ls data/accounts/3230304/devices/
# Should show: I6332527703739342000020/ (not A81B6A536A98/)
cat data/accounts/3230304/devices/*/DeviceInfo.xml | grep macAddress
For detailed diagnosis and solutions, see: MAC Address Mapping Guide
When you migrate a speaker using the resolv.conf method, the service needs to write a raw IP address into the speaker’s network configuration. That IP must be the address the speaker itself can reach — which is not necessarily the same address your computer resolves.
In environments with NAT, split-horizon DNS, or Docker/container networking, soundtouch.local (or whatever you set as SERVER_URL) may resolve to a different IP depending on who is asking. The service therefore resolves the hostname by running ping -c 1 <hostname> over SSH on the speaker and extracting the IP from the output. This is the authoritative result: it is exactly what the speaker would use.
If that SSH ping fails, migration is aborted. Writing an unresolvable or incorrectly resolved hostname into aftertouch.resolv.conf would silently break the speaker’s DNS config and prevent it from reaching the service after reboot.
The XML migration method is different. It writes the full URL (e.g. http://soundtouch.local:8000) into SoundTouchSdkPrivateCfg.xml. The speaker resolves the hostname at connect time, not at migration time. This means migration can proceed even if the hostname is not yet reachable — for example, when the service will be deployed under that hostname but is not running yet. A warning is still shown in the UI so you are aware, but the Confirm Migration button remains enabled.
Symptoms (migration log or web UI warning):
cannot resolve target hostname for migration: cannot resolve "soundtouch.local":
SSH ping from device failed and service-side DNS lookup also failed
or:
resolved "soundtouch.local" to 192.168.1.100 from service, not from device —
result may be wrong if NAT or split-DNS is in use
What this means:
The service could not confirm the IP by running ping on the speaker via SSH. Either:
ping binary is not available or not in $PATH on this firmware, orDiagnosis — run manually over SSH:
# SSH into the speaker
ssh root@<speaker-ip>
# Try to resolve the service hostname
ping -c 1 soundtouch.local
# or use the IP directly to verify connectivity
ping -c 1 192.168.1.100
# Check the speaker's current DNS config
cat /etc/resolv.conf
# Check if ping is available
which ping
busybox ping --help
Solutions:
The most reliable fix. If the hostname cannot be resolved from the device, use a raw IP instead. Resolution is skipped entirely when SERVER_URL contains an IP.
# In your .env
SERVER_URL=http://192.168.1.100:8000
HTTPS_SERVER_URL=https://192.168.1.100:8443
HTTPS works correctly with IP addresses — the service certificate includes the IP as a Subject Alternative Name (SAN).
If you use soundtouch.local, verify mDNS is working from another device on the same subnet:
avahi-resolve -n soundtouch.local # Linux
dns-sd -G v4 soundtouch.local # macOS
Select the XML method in the migration UI. It writes the full URL and the speaker resolves it at connect time, so hostname resolution is not required during migration. This also allows migrating to a hostname that is not yet live.
When reporting issues, include:
// Device information
info, _ := client.GetDeviceInfo()
fmt.Printf("Device: %s %s (ID: %s)\n", info.Type, info.Name, info.DeviceID)
// Network information
network, _ := client.GetNetworkInfo()
fmt.Printf("Network: %+v\n", network)
// Go version and OS
fmt.Printf("Go version: %s\n", runtime.Version())
fmt.Printf("OS: %s/%s\n", runtime.GOOS, runtime.GOARCH)
# System information
go version
uname -a # Linux/macOS
systeminfo # Windows
# Network debugging
ip addr show # Linux
ifconfig # macOS
ipconfig /all # Windows
# SoundTouch specific
go run ./cmd/soundtouch-cli -host <ip> -info
go run ./cmd/soundtouch-cli -host <ip> -network-info
/docs directory for specific topics/examples for working code patternsRemember: Most issues are network-related. Start with basic connectivity testing before investigating code issues.