Documentation for controlling and preserving Bose SoundTouch devices
This document describes the comprehensive zone management functionality for controlling multiroom setups with Bose SoundTouch devices.
Zone management allows you to:
All SoundTouch devices that support multiroom functionality can participate in zones, with one device acting as the master and others as members.
package main
import (
"fmt"
"log"
"github.com/gesellix/bose-soundtouch/pkg/client"
"github.com/gesellix/bose-soundtouch/pkg/models"
)
func main() {
// Connect to SoundTouch device
soundTouchClient := client.NewClientFromHost("192.168.1.10")
// Get current zone information
zone, err := soundTouchClient.GetZone()
if err != nil {
log.Fatal(err)
}
fmt.Printf("Zone Master: %s\n", zone.Master)
fmt.Printf("Zone Members: %d\n", len(zone.Members))
if zone.IsStandalone() {
fmt.Println("Device is standalone (not in a zone)")
} else {
fmt.Printf("Total devices in zone: %d\n", zone.GetTotalDeviceCount())
}
}
# Get zone information
go run ./cmd/soundtouch-cli -host 192.168.1.10 -zone
# Check zone status for this device
go run ./cmd/soundtouch-cli -host 192.168.1.10 -zone-status
# List all zone members
go run ./cmd/soundtouch-cli -host 192.168.1.10 -zone-members
# Create a new zone
go run ./cmd/soundtouch-cli -host 192.168.1.10 -create-zone MASTER123,MEMBER456,MEMBER789
# Add device to existing zone
go run ./cmd/soundtouch-cli -host 192.168.1.10 -add-to-zone DEVICE456@192.168.1.11
# Remove device from zone
go run ./cmd/soundtouch-cli -host 192.168.1.10 -remove-from-zone DEVICE456
# Dissolve current zone
go run ./cmd/soundtouch-cli -host 192.168.1.10 -dissolve-zone
| State | Description |
|---|---|
STANDALONE |
Device operates independently |
MASTER |
Device controls a multiroom zone |
SLAVE |
Device follows zone master |
zone, err := client.GetZone()
if err != nil {
log.Fatal(err)
}
fmt.Printf("Master: %s\n", zone.Master)
for i, member := range zone.Members {
fmt.Printf("Member %d: %s (%s)\n", i+1, member.DeviceID, member.IP)
}
Response Structure:
<zone master="ABCD1234EFGH">
<member ipaddress="192.168.1.11">EFGH5678IJKL</member>
<member ipaddress="192.168.1.12">IJKL9012MNOP</member>
</zone>
status, err := client.GetZoneStatus()
if err != nil {
log.Fatal(err)
}
fmt.Printf("This device is: %s\n", status.String())
members, err := client.GetZoneMembers()
if err != nil {
log.Fatal(err)
}
fmt.Printf("Zone has %d devices:\n", len(members))
for _, deviceID := range members {
fmt.Printf(" - %s\n", deviceID)
}
// Simple zone creation
masterID := "ABCD1234EFGH"
memberIDs := []string{"EFGH5678IJKL", "IJKL9012MNOP"}
err := client.CreateZone(masterID, memberIDs)
if err != nil {
log.Fatal(err)
}
masterID := "ABCD1234EFGH"
members := map[string]string{
"EFGH5678IJKL": "192.168.1.11",
"IJKL9012MNOP": "192.168.1.12",
}
err := client.CreateZoneWithIPs(masterID, members)
if err != nil {
log.Fatal(err)
}
// Add device with IP address
err := client.AddToZone("DEVICE789", "192.168.1.13")
if err != nil {
log.Fatal(err)
}
err := client.RemoveFromZone("DEVICE789")
if err != nil {
log.Fatal(err)
}
// Make all devices standalone
err := client.DissolveZone()
if err != nil {
log.Fatal(err)
}
zoneRequest, err := models.NewZoneBuilder("MASTER123").
WithMember("DEVICE456", "192.168.1.11").
WithMemberByDeviceID("DEVICE789").
Build()
if err != nil {
log.Fatal(err)
}
err = client.SetZone(zoneRequest)
if err != nil {
log.Fatal(err)
}
// Create zone request manually
zoneRequest := models.NewZoneRequest("MASTER123")
zoneRequest.AddMember("DEVICE456", "192.168.1.11")
zoneRequest.AddMember("DEVICE789", "192.168.1.12")
// Validate before sending
if err := zoneRequest.Validate(); err != nil {
log.Fatal(err)
}
err := client.SetZone(zoneRequest)
if err != nil {
log.Fatal(err)
}
zone, _ := client.GetZone()
// Check zone status
if zone.IsStandalone() {
fmt.Println("No multiroom zone active")
}
// Check device membership
if zone.IsMaster("DEVICE123") {
fmt.Println("DEVICE123 is the zone master")
}
if zone.IsMember("DEVICE456") {
fmt.Println("DEVICE456 is a zone member")
}
// Find device by IP
member, found := zone.GetMemberByIP("192.168.1.11")
if found {
fmt.Printf("Device at 192.168.1.11 is %s\n", member.DeviceID)
}
// Get all device IDs
allDevices := zone.GetAllDeviceIDs()
fmt.Printf("Zone contains: %v\n", allDevices)
wsClient := soundTouchClient.NewWebSocketClient(nil)
// Monitor zone changes
wsClient.OnZoneUpdated(func(event *models.ZoneUpdatedEvent) {
zone := &event.Zone
if zone.Master != "" {
fmt.Printf("Zone updated - Master: %s\n", zone.Master)
fmt.Printf("Members: %d\n", len(zone.Members))
for _, member := range zone.Members {
fmt.Printf(" - %s (%s)\n", member.DeviceID, member.IP)
}
} else {
fmt.Println("Zone dissolved - device is now standalone")
}
})
// Connect and start monitoring
wsClient.Connect()
defer wsClient.Disconnect()
err := client.AddToZone("INVALID_DEVICE", "192.168.1.99")
if err != nil {
// Handle specific zone errors
if zoneErr, ok := err.(*models.ZoneError); ok {
fmt.Printf("Zone operation %s failed: %s\n",
zoneErr.Operation, zoneErr.Reason)
switch zoneErr.Reason {
case models.ZoneErrorDeviceNotFound:
fmt.Println("Device not found on network")
case models.ZoneErrorDeviceOffline:
fmt.Println("Device is offline")
case models.ZoneErrorAlreadyInZone:
fmt.Println("Device already in a zone")
case models.ZoneErrorMaxMembersReached:
fmt.Println("Maximum zone size reached")
}
}
}
zoneRequest := models.NewZoneRequest("") // Invalid: empty master
if err := zoneRequest.Validate(); err != nil {
fmt.Printf("Zone validation failed: %v\n", err)
// Error: "master device ID is required"
}
// Robust zone creation with retry
func createZoneWithRetry(client *client.Client, master string, members []string) error {
maxRetries := 3
for i := 0; i < maxRetries; i++ {
err := client.CreateZone(master, members)
if err == nil {
return nil
}
// Handle specific errors
if zoneErr, ok := err.(*models.ZoneError); ok {
switch zoneErr.Reason {
case models.ZoneErrorNetworkError:
// Retry on network errors
time.Sleep(time.Second * 2)
continue
case models.ZoneErrorDeviceNotFound:
// Don't retry on device not found
return err
}
}
time.Sleep(time.Second * 2)
}
return fmt.Errorf("failed to create zone after %d retries", maxRetries)
}
// Check if device is in zone efficiently
func isDeviceInZone(client *client.Client) (bool, error) {
// More efficient than getting full zone info
return client.IsInZone()
}
// Get zone member count without full member list
func getZoneMemberCount(client *client.Client) (int, error) {
zone, err := client.GetZone()
if err != nil {
return 0, err
}
return zone.GetTotalDeviceCount(), nil
}
// Automatically create zones based on room groupings
func createRoomZones() {
livingRoomMaster := "LIVING_ROOM_MAIN"
livingRoomMembers := []string{"LIVING_ROOM_LEFT", "LIVING_ROOM_RIGHT"}
kitchenMaster := "KITCHEN_MAIN"
kitchenMembers := []string{"KITCHEN_COUNTER"}
// Create living room zone
livingRoomClient := client.NewClientFromHost("192.168.1.10")
livingRoomClient.CreateZone(livingRoomMaster, livingRoomMembers)
// Create kitchen zone
kitchenClient := client.NewClientFromHost("192.168.1.20")
kitchenClient.CreateZone(kitchenMaster, kitchenMembers)
}
// Create house-wide party zone
func enablePartyMode() {
allDevices := []string{
"LIVING_ROOM", "KITCHEN", "BEDROOM",
"BATHROOM", "OFFICE", "BASEMENT",
}
if len(allDevices) > 0 {
master := allDevices[0]
members := allDevices[1:]
masterClient := client.NewClientFromHost("192.168.1.10")
err := masterClient.CreateZone(master, members)
if err != nil {
log.Printf("Failed to create party zone: %v", err)
}
}
}
func disablePartyMode() {
// Dissolve all zones
for _, ip := range []string{"192.168.1.10", "192.168.1.11", "192.168.1.12"} {
client := client.NewClientFromHost(ip)
client.DissolveZone()
}
}
// Move zone to follow user between rooms
func moveZoneToRoom(currentClient, targetClient *client.Client, targetDeviceID string) error {
// Get current zone
zone, err := currentClient.GetZone()
if err != nil {
return err
}
// Add target device to zone
err = currentClient.AddToZone(targetDeviceID, "")
if err != nil {
return err
}
// Wait for synchronization
time.Sleep(time.Second * 2)
// Make target device the new master
newZoneRequest := models.NewZoneRequest(targetDeviceID)
for _, member := range zone.Members {
if member.DeviceID != targetDeviceID {
newZoneRequest.AddMember(member.DeviceID, member.IP)
}
}
return targetClient.SetZone(newZoneRequest)
}
func debugZoneStatus(client *client.Client) {
// Get comprehensive zone information
zone, err := client.GetZone()
if err != nil {
fmt.Printf("Error getting zone: %v\n", err)
return
}
fmt.Printf("Zone Debug Information:\n")
fmt.Printf(" Master: %s\n", zone.Master)
fmt.Printf(" Members: %d\n", len(zone.Members))
fmt.Printf(" Total Devices: %d\n", zone.GetTotalDeviceCount())
fmt.Printf(" Is Standalone: %t\n", zone.IsStandalone())
for i, member := range zone.Members {
fmt.Printf(" Member %d: %s (IP: %s)\n",
i+1, member.DeviceID, member.IP)
}
// Check device status
status, err := client.GetZoneStatus()
if err == nil {
fmt.Printf(" This Device Status: %s\n", status.String())
}
}
// Test zone functionality
func testZoneOperations(client *client.Client) {
fmt.Println("Testing zone operations...")
// Test 1: Get initial status
initialZone, err := client.GetZone()
if err != nil {
fmt.Printf("❌ Failed to get initial zone: %v\n", err)
return
}
fmt.Printf("✅ Initial zone status: %s\n", initialZone.String())
// Test 2: Check capabilities
inZone, err := client.IsInZone()
if err != nil {
fmt.Printf("❌ Failed to check zone membership: %v\n", err)
return
}
fmt.Printf("✅ Zone membership check: %t\n", inZone)
// Test 3: Get zone status
status, err := client.GetZoneStatus()
if err != nil {
fmt.Printf("❌ Failed to get zone status: %v\n", err)
return
}
fmt.Printf("✅ Zone status: %s\n", status.String())
fmt.Println("All zone tests passed!")
}
The zone management implementation provides comprehensive multiroom control with robust error handling, validation, and real-time monitoring capabilities for production-ready applications.