mirror of
https://github.com/gesellix/Bose-SoundTouch.git
synced 2026-08-10 20:56:14 +00:00
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>
158 lines
5.3 KiB
Go
158 lines
5.3 KiB
Go
package models
|
|
|
|
import (
|
|
"encoding/xml"
|
|
"time"
|
|
)
|
|
|
|
// DeviceInfo represents the response from GET /info endpoint
|
|
type DeviceInfo struct {
|
|
XMLName xml.Name `xml:"info" json:"-"`
|
|
DeviceID string `xml:"deviceID,attr" json:"device_id"`
|
|
Name string `xml:"name" json:"name"`
|
|
Type string `xml:"type" json:"type"`
|
|
MargeAccountUUID string `xml:"margeAccountUUID" json:"marge_account_uuid,omitempty"`
|
|
Components []Component `xml:"components>component" json:"components,omitempty"`
|
|
MargeURL string `xml:"margeURL" json:"marge_url,omitempty"`
|
|
NetworkInfo []NetworkInfo `xml:"networkInfo" json:"network_info,omitempty"`
|
|
ModuleType string `xml:"moduleType" json:"module_type,omitempty"`
|
|
Variant string `xml:"variant" json:"variant,omitempty"`
|
|
VariantMode string `xml:"variantMode" json:"variant_mode,omitempty"`
|
|
CountryCode string `xml:"countryCode" json:"country_code,omitempty"`
|
|
RegionCode string `xml:"regionCode" json:"region_code,omitempty"`
|
|
IPAddress string `xml:"-" json:"ip_address,omitempty"`
|
|
}
|
|
|
|
// Component represents a device component
|
|
type Component struct {
|
|
ComponentCategory string `xml:"componentCategory" json:"component_category"`
|
|
SoftwareVersion string `xml:"softwareVersion" json:"software_version"`
|
|
SerialNumber string `xml:"serialNumber" json:"serial_number"`
|
|
}
|
|
|
|
// NetworkInfo represents network information for the device
|
|
type NetworkInfo struct {
|
|
Type string `xml:"type,attr" json:"type"`
|
|
MacAddress string `xml:"macAddress" json:"mac_address"`
|
|
IPAddress string `xml:"ipAddress" json:"ip_address"`
|
|
}
|
|
|
|
// SourcesUpdatedNotification represents the notification XML sent to the device
|
|
type SourcesUpdatedNotification struct {
|
|
XMLName xml.Name `xml:"updates"`
|
|
DeviceID string `xml:"deviceID,attr"`
|
|
Sources struct {
|
|
XMLName xml.Name `xml:"sourcesUpdated"`
|
|
} `xml:"sourcesUpdated"`
|
|
}
|
|
|
|
// NewSourcesUpdatedNotification creates a new sources updated notification
|
|
func NewSourcesUpdatedNotification(deviceID string) *SourcesUpdatedNotification {
|
|
return &SourcesUpdatedNotification{
|
|
DeviceID: deviceID,
|
|
}
|
|
}
|
|
|
|
// XMLResponse is a generic wrapper for API responses
|
|
type XMLResponse struct {
|
|
XMLName xml.Name
|
|
Error *APIError `xml:"error,omitempty"`
|
|
}
|
|
|
|
// APIError represents an error response from the API
|
|
type APIError struct {
|
|
Code int `xml:"code,attr"`
|
|
Message string `xml:",chardata"`
|
|
}
|
|
|
|
// Error implements the error interface
|
|
func (e *APIError) Error() string {
|
|
return e.Message
|
|
}
|
|
|
|
// ErrorsResponse represents a multi-error response from the API (common in some firmware versions)
|
|
type ErrorsResponse struct {
|
|
XMLName xml.Name `xml:"errors"`
|
|
DeviceID string `xml:"deviceID,attr"`
|
|
Errors []DeviceError `xml:"error"`
|
|
}
|
|
|
|
// Error implements the error interface for ErrorsResponse
|
|
func (e *ErrorsResponse) Error() string {
|
|
if len(e.Errors) > 0 {
|
|
return e.Errors[0].Message
|
|
}
|
|
|
|
return "unknown API error"
|
|
}
|
|
|
|
// DeviceError represents a single error in an ErrorsResponse
|
|
type DeviceError struct {
|
|
Value int `xml:"value,attr"`
|
|
Name string `xml:"name,attr"`
|
|
Message string `xml:",chardata"`
|
|
}
|
|
|
|
// DiscoveredDevice represents a device found through network discovery
|
|
type DiscoveredDevice struct {
|
|
Name string `json:"name"`
|
|
Host string `json:"host"`
|
|
Port int `json:"port"`
|
|
ModelID string `json:"model_id"`
|
|
SerialNo string `json:"serial_no"`
|
|
LastSeen time.Time `json:"last_seen"`
|
|
DiscoveryMethod string `json:"discovery_method"`
|
|
|
|
// Standard URLs
|
|
APIBaseURL string `json:"api_base_url"` // http://host:port/
|
|
InfoURL string `json:"info_url"` // http://host:port/info
|
|
|
|
// Protocol-specific details
|
|
UPnPLocation string `json:"upnp_location,omitempty"` // UPnP device description XML URL
|
|
UPnPUSN string `json:"upnp_usn,omitempty"` // UPnP Unique Service Name
|
|
UPnPSerial string `json:"upnp_serial,omitempty"` // Serial number from UPnP (MAC address)
|
|
Manufacturer string `json:"manufacturer,omitempty"` // Manufacturer from UPnP device description (used to reject non-Bose devices)
|
|
MDNSHostname string `json:"mdns_hostname,omitempty"` // mDNS hostname (e.g., "device.local.")
|
|
MDNSService string `json:"mdns_service,omitempty"` // mDNS service name
|
|
ConfigName string `json:"config_name,omitempty"` // Original name from config
|
|
|
|
// Additional metadata
|
|
Metadata map[string]string `json:"metadata,omitempty"`
|
|
}
|
|
|
|
// GetStandardURLs returns the standard API URLs for this device
|
|
func (d *DiscoveredDevice) GetStandardURLs() map[string]string {
|
|
return map[string]string{
|
|
"base": d.APIBaseURL,
|
|
"info": d.InfoURL,
|
|
}
|
|
}
|
|
|
|
// GetProtocolSpecificData returns protocol-specific information
|
|
func (d *DiscoveredDevice) GetProtocolSpecificData() map[string]interface{} {
|
|
data := make(map[string]interface{})
|
|
|
|
if d.UPnPLocation != "" {
|
|
data["upnp"] = map[string]string{
|
|
"location": d.UPnPLocation,
|
|
"usn": d.UPnPUSN,
|
|
"serial": d.UPnPSerial,
|
|
}
|
|
}
|
|
|
|
if d.MDNSHostname != "" {
|
|
data["mdns"] = map[string]string{
|
|
"hostname": d.MDNSHostname,
|
|
"service": d.MDNSService,
|
|
}
|
|
}
|
|
|
|
if d.ConfigName != "" {
|
|
data["config"] = map[string]string{
|
|
"original_name": d.ConfigName,
|
|
}
|
|
}
|
|
|
|
return data
|
|
}
|