Files
Bose-SoundTouch/pkg/discovery/constants.go
Tobias Gesellchen 1cd4226f5b feat: tighter discovery filter + UX cleanups (#269, #345, #355, #359)
Four small, independent improvements bundled into one cut:

1. Restrict device discovery to SoundTouch-family services (#269/#359).
   - mDNS now queries all three SoundTouch service-type variants in
     parallel (_soundtouch._tcp, _bose-soundtouch._tcp, _soundtouchstick._tcp)
     and deduplicates results by host:port. mDNS has no native wildcard
     for service types, so we fan out one query per variant.
   - UPnP/SSDP M-SEARCH receives a manufacturer/modelName check after
     fetching the device description: devices whose manufacturer doesn't
     contain "bose" AND whose model doesn't contain "soundtouch" are
     rejected. Closes the loop on NorbertBauer's diagnostic bundle that
     showed a Dreambox dm920 and Onkyo HT-R695 living under the default
     account because they answered our generic MediaRenderer:1 probe.

2. New health check: default-account-contains-non-Bose-devices (#269).
   Walks devices keyed under data/accounts/default/devices/, flags any
   whose ProductCode/Name doesn't look SoundTouch, and offers an Evict
   QuickFix. Bose devices still in default (legitimate pre-pair) are
   intentionally ignored — that's the consistency check's domain.

3. Clipboard fallback for Copy buttons (#355). The two health-tab Copy
   buttons used navigator.clipboard.writeText, which requires a secure
   context. Over plain HTTP at a LAN IP the browser blocks it silently
   and the button shows "Copy failed". New copyTextToClipboard helper
   tries the modern API first, falls back to document.execCommand("copy")
   via an off-screen textarea.

4. Web UI static-asset cache-busting (#345). dekiesel needed Ctrl+F5 to
   see the v0.89 Download button after upgrade. The root HTML now
   carries a ?v=<hash> query string on /web/js/script.js and
   /web/css/style.css references. Hash is sha256 over the embedded asset
   bodies, truncated to 12 hex chars — stable per binary, changes when
   the assets change.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 21:15:34 +02:00

59 lines
2.0 KiB
Go

// Package discovery provides device discovery functionality for Bose SoundTouch devices using mDNS and UPnP protocols.
package discovery
import (
"strings"
"time"
)
const (
// SSDP multicast address and port
ssdpAddr = "239.255.255.250:1900"
// SoundTouch device URN for UPnP discovery
soundTouchURN = "urn:schemas-upnp-org:device:MediaRenderer:1"
// mDNS service type for SoundTouch devices (matches Bose's actual service name).
// Retained as the canonical / primary service type for log lines and tests;
// the full set of accepted variants lives in soundTouchServiceTypes below.
soundTouchServiceType = "_soundtouch._tcp"
soundTouchDomain = "local."
// Default discovery timeout
defaultTimeout = 5 * time.Second
// Default cache TTL
defaultCacheTTL = 30 * time.Second
)
// soundTouchServiceTypes lists every mDNS service-type variant we consider
// part of the SoundTouch family. mDNS doesn't support wildcard service-type
// queries at the protocol level, so the discovery code issues one parallel
// query per entry below and merges the results. Add new variants here as
// they're observed in the wild — Bose has historically advertised at
// least three:
//
// - _soundtouch._tcp : classic SoundTouch speakers (ST10/20/30, …)
// - _bose-soundtouch._tcp : seen on some newer firmware variants
// - _soundtouchstick._tcp : SoundTouch Wireless Adapter / dongle
var soundTouchServiceTypes = []string{
"_soundtouch._tcp",
"_bose-soundtouch._tcp",
"_soundtouchstick._tcp",
}
// isSoundTouchServiceName reports whether the mDNS service entry name
// belongs to any registered SoundTouch service type. Case-insensitive
// substring match — Bose's mDNS entries embed the service type after a
// dot (e.g. "Speaker._soundtouch._tcp.local.").
func isSoundTouchServiceName(name string) bool {
lower := strings.ToLower(name)
for _, t := range soundTouchServiceTypes {
if strings.Contains(lower, strings.ToLower(t)) {
return true
}
}
return false
}