From 3b04a6eb57fc51831e9605ddffa9dddb9e5ebf31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Lipinsk=C3=BD?= <6032558+Mr-Tao@users.noreply.github.com> Date: Thu, 27 Aug 2026 14:31:54 +0200 Subject: [PATCH] fix(player): skip stereo polling on unsupported models --- .../soundtouchweb/device_projection.go | 10 ++++ pkg/service/soundtouchweb/websocket.go | 22 ++++++-- pkg/service/soundtouchweb/websocket_test.go | 50 ++++++++++++++++++- 3 files changed, 76 insertions(+), 6 deletions(-) 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)