fix(player): retry configured devices during discovery

This commit is contained in:
Lukáš Lipinský
2026-08-23 22:00:11 +02:00
committed by Tobias Gesellchen
parent d3c5ad2d8e
commit 7015e04556
4 changed files with 80 additions and 9 deletions
+2 -2
View File
@@ -88,7 +88,7 @@ go build -o soundtouch-player
./soundtouch-player -port 8888
# Connect to specific device
./soundtouch-player -host 192.0.2.100
./soundtouch-player --devices 192.0.2.100
```
### Command Line Options
@@ -137,7 +137,7 @@ device datastore).
The application automatically discovers SoundTouch devices using:
- **mDNS discovery** for local network devices
- **UPnP/SSDP discovery** as fallback
- **Manual device addition** via IP address
- **Configured devices** via `--devices`, retried whenever discovery runs
### Real-time Updates
The interface maintains WebSocket connections to each device for instant updates of:
+1 -5
View File
@@ -161,7 +161,7 @@ func main() {
log.Printf("Trusting AfterTouch service CA from %s", sanitizeLog(caPath))
}
discoveryService := soundtouchweb.NewDiscoveryService(ifaceName)
discoveryService := soundtouchweb.NewDiscoveryService(ifaceName, manualHosts...)
// Discover devices on startup
go func() {
@@ -170,10 +170,6 @@ func main() {
webApp.BroadcastDiscoveryStatus("starting", webApp.DeviceCount())
for _, host := range manualHosts {
webApp.AddDeviceByHost(host, 8090, "manual")
}
webApp.DiscoverDevices(ctx, discoveryService)
webApp.BroadcastDiscoveryStatus("completed", webApp.DeviceCount())
+19 -2
View File
@@ -10,12 +10,13 @@ import (
"github.com/gesellix/bose-soundtouch/pkg/config"
"github.com/gesellix/bose-soundtouch/pkg/discovery"
"github.com/gesellix/bose-soundtouch/pkg/service/soundtouchweb/webtypes"
"github.com/gesellix/bose-soundtouch/pkg/speaker"
)
// NewDiscoveryService loads config and returns a unified discovery service
// preconfigured for the web UI's use (10 s discovery timeout, cache on).
// When discoveryInterface is non-empty, mDNS/UPnP are pinned to that NIC.
func NewDiscoveryService(discoveryInterface string) *discovery.UnifiedDiscoveryService {
func NewDiscoveryService(discoveryInterface string, configuredHosts ...string) *discovery.UnifiedDiscoveryService {
cfg, err := config.LoadFromEnv()
if err != nil {
log.Printf("Failed to load config: %v, using defaults", err)
@@ -30,6 +31,17 @@ func NewDiscoveryService(discoveryInterface string) *discovery.UnifiedDiscoveryS
cfg.DiscoveryInterface = discoveryInterface
}
for _, host := range configuredHosts {
if host == "" {
continue
}
cfg.PreferredDevices = append(cfg.PreferredDevices, config.DeviceConfig{
Host: host,
Port: speaker.HTTPPort,
})
}
return discovery.NewUnifiedDiscoveryService(cfg)
}
@@ -164,6 +176,11 @@ func (app *WebApp) DiscoverDevices(ctx context.Context, discoveryService *discov
log.Printf("Found %d devices", len(devices))
for _, device := range devices {
app.AddDeviceByHost(device.Host, device.Port, "discovered")
source := "discovered"
if device.DiscoveryMethod == "Configuration" {
source = "manual"
}
app.AddDeviceByHost(device.Host, device.Port, source)
}
}
@@ -0,0 +1,58 @@
package soundtouchweb
import (
"context"
"net/http"
"net/http/httptest"
"strings"
"sync/atomic"
"testing"
)
func TestDiscoverDevicesRetriesConfiguredHosts(t *testing.T) {
var available atomic.Bool
var infoRequests atomic.Int32
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/info" {
http.NotFound(w, r)
return
}
infoRequests.Add(1)
if !available.Load() {
http.Error(w, "offline", http.StatusServiceUnavailable)
return
}
w.Header().Set("Content-Type", "application/xml")
_, _ = w.Write([]byte(`<info deviceID="TESTDEVICE"><name>Configured speaker</name><type>SoundTouch 10</type></info>`))
}))
defer server.Close()
t.Setenv("UPNP_ENABLED", "false")
t.Setenv("MDNS_ENABLED", "false")
t.Setenv("PREFERRED_DEVICES", "")
configuredHost := strings.TrimPrefix(server.URL, "http://")
discoveryService := NewDiscoveryService("", configuredHost)
app := NewWebApp()
app.DiscoverDevices(context.Background(), discoveryService)
if got := app.DeviceCount(); got != 0 {
t.Fatalf("device count after offline probe = %d, want 0", got)
}
available.Store(true)
app.DiscoverDevices(context.Background(), discoveryService)
if got := app.DeviceCount(); got != 1 {
t.Fatalf("device count after retry = %d, want 1", got)
}
if got := infoRequests.Load(); got != 2 {
t.Fatalf("/info request count = %d, want 2", got)
}
if !app.RemoveDevice(configuredHost) {
t.Fatal("configured device was not registered under its host")
}
}