diff --git a/pkg/service/soundtouchweb/device_projection.go b/pkg/service/soundtouchweb/device_projection.go
index 6e453c8c..e5e28b8f 100644
--- a/pkg/service/soundtouchweb/device_projection.go
+++ b/pkg/service/soundtouchweb/device_projection.go
@@ -52,6 +52,16 @@ type stereoPairMemberView struct {
Available bool `json:"available"`
}
+func stereoPairCapable(info *models.DeviceInfo) bool {
+ if info == nil {
+ return false
+ }
+
+ typeName := strings.ToLower(strings.TrimSpace(info.Type))
+
+ return typeName == "st10" || typeName == "soundtouch 10"
+}
+
// deviceViewSnapshot projects the physical registry into logical control
// targets for the HTTP API and the global player WebSocket.
func (app *WebApp) deviceViewSnapshot() map[string]deviceView {
diff --git a/pkg/service/soundtouchweb/websocket.go b/pkg/service/soundtouchweb/websocket.go
index 4ae50099..9df2b365 100644
--- a/pkg/service/soundtouchweb/websocket.go
+++ b/pkg/service/soundtouchweb/websocket.go
@@ -303,7 +303,13 @@ func (app *WebApp) UpdateDeviceStatus(_ string, conn *webtypes.DeviceConnection)
return
}
- groupGeneration := conn.BeginGroupRefresh()
+ // /getGroup is ST10-only; ST20/ST30 may accept the request but never reply.
+ stereoCapable := stereoPairCapable(conn.DeviceInfo)
+
+ var groupGeneration uint64
+ if stereoCapable {
+ groupGeneration = conn.BeginGroupRefresh()
+ }
// Phase 1: slow network fetches. Local vars only, no shared state
// is touched yet. Errors are recorded so the merge below can tell
@@ -313,7 +319,15 @@ func (app *WebApp) UpdateDeviceStatus(_ string, conn *webtypes.DeviceConnection)
presets, presetsErr := conn.Client.GetPresets()
sources, sourcesErr := conn.Client.GetSources()
bass, bassErr := conn.Client.GetBass()
- group, groupErr := conn.Client.GetGroup()
+
+ var (
+ group *models.Group
+ groupErr error
+ )
+
+ if stereoCapable {
+ group, groupErr = conn.Client.GetGroup()
+ }
// Phase 2: fast merge. Only fields we successfully fetched
// overwrite; everything else keeps the value other goroutines may
@@ -346,7 +360,7 @@ func (app *WebApp) UpdateDeviceStatus(_ string, conn *webtypes.DeviceConnection)
statusUpdated = true
}
- statusUpdated = statusUpdated || groupErr == nil
+ statusUpdated = statusUpdated || (stereoCapable && groupErr == nil)
// Mark as connected if we successfully got at least one
// status from this round. Mirrors prior behaviour.
@@ -354,7 +368,7 @@ func (app *WebApp) UpdateDeviceStatus(_ string, conn *webtypes.DeviceConnection)
s.LastActivity = time.Now()
})
- if groupErr == nil {
+ if stereoCapable && groupErr == nil {
conn.ApplyPolledGroup(groupGeneration, group)
}
}
diff --git a/pkg/service/soundtouchweb/websocket_test.go b/pkg/service/soundtouchweb/websocket_test.go
index 405ed00e..062ca494 100644
--- a/pkg/service/soundtouchweb/websocket_test.go
+++ b/pkg/service/soundtouchweb/websocket_test.go
@@ -3,6 +3,7 @@ package soundtouchweb
import (
"net/http"
"net/http/httptest"
+ "sync/atomic"
"testing"
"time"
@@ -23,7 +24,7 @@ func TestUpdateDeviceStatusRefreshesGroup(t *testing.T) {
`)
defer server.Close()
- conn := webtypes.NewDeviceConnection(client.NewClientFromHost(server.URL), nil)
+ conn := webtypes.NewDeviceConnection(client.NewClientFromHost(server.URL), &models.DeviceInfo{Type: "SoundTouch 10"})
NewWebApp().UpdateDeviceStatus("device-1", conn)
status := conn.Status()
@@ -48,7 +49,7 @@ func TestUpdateDeviceStatusPreservesGroupOnError(t *testing.T) {
server := newStatusTestServer(t, http.StatusInternalServerError, "group unavailable")
defer server.Close()
- conn := webtypes.NewDeviceConnection(client.NewClientFromHost(server.URL), nil)
+ conn := webtypes.NewDeviceConnection(client.NewClientFromHost(server.URL), &models.DeviceInfo{Type: "SoundTouch 10"})
existing := &models.Group{ID: "pair-old", Name: "Existing Pair"}
conn.SetStatus(&webtypes.DeviceStatus{Group: existing})
@@ -64,6 +65,51 @@ func TestUpdateDeviceStatusPreservesGroupOnError(t *testing.T) {
}
}
+func TestUpdateDeviceStatusSkipsGroupForNonStereoModel(t *testing.T) {
+ var groupRequested atomic.Bool
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if r.URL.Path == "/getGroup" {
+ groupRequested.Store(true)
+ http.Error(w, "unsupported endpoint", http.StatusInternalServerError)
+
+ return
+ }
+
+ responses := map[string]string{
+ "/now_playing": `STOP_STATE`,
+ "/volume": `1010false`,
+ "/presets": ``,
+ "/sources": ``,
+ "/bass": `00`,
+ }
+ body, ok := responses[r.URL.Path]
+ if !ok {
+ http.NotFound(w, r)
+
+ return
+ }
+
+ w.Header().Set("Content-Type", "application/xml")
+ _, _ = w.Write([]byte(body))
+ }))
+ defer server.Close()
+
+ for _, model := range []string{"SoundTouch 20", "SoundTouch 30"} {
+ t.Run(model, func(t *testing.T) {
+ conn := webtypes.NewDeviceConnection(client.NewClientFromHost(server.URL), &models.DeviceInfo{Type: model})
+ NewWebApp().UpdateDeviceStatus("device-1", conn)
+
+ if !conn.Status().IsConnected {
+ t.Fatal("successful ordinary status requests should mark a non-stereo model connected")
+ }
+ })
+ }
+
+ if groupRequested.Load() {
+ t.Fatal("UpdateDeviceStatus requested /getGroup for a non-stereo model")
+ }
+}
+
func TestApplyGroupUpdatedEventReplacesGroup(t *testing.T) {
conn := webtypes.NewDeviceConnection(nil, nil)
previousActivity := time.Unix(1, 0)