mirror of
https://github.com/gesellix/Bose-SoundTouch.git
synced 2026-08-18 08:36:13 +00:00
Major improvements to device discovery system: 🔧 **SSDP Discovery Fixed**: - Fixed networking issue where SSDP used connected UDP socket instead of UDP listener - SSDP now properly receives unicast responses from multicast requests - UPnP discovery now works reliably and finds all MediaRenderer devices ✨ **Enhanced DiscoveredDevice Model**: - Added consistent URL fields (APIBaseURL, InfoURL) for all discovery methods - Added protocol-specific fields (UPnPLocation, UPnPUSN, MDNSHostname, etc.) - Added DiscoveryMethod tracking to show how devices were found - Added device merging support for same device found via multiple protocols 🚀 **Unified Discovery Improvements**: - Fixed device merging logic to properly combine protocol-specific data - Discovery methods now correctly show combinations like 'Configuration+SSDP/UPnP+mDNS/Bonjour' - Removed duplicate configuration device loading in individual services - All three discovery methods (SSDP, mDNS, Configuration) work together seamlessly 🛠 **Updated Tools & Examples**: - Updated soundtouch-cli to display new consistent field structure - Enhanced all example programs with better device information display - Added new unified discovery example demonstrating all three methods - Fixed context timeout issues in example programs 📋 **Comprehensive Testing**: - All tests updated and passing - Real-world validation with actual Bose SoundTouch devices - Confirmed discovery methods properly merge device data Every discovered device now has consistent http://host:port/info URLs regardless of discovery method, while preserving valuable protocol-specific metadata.
263 lines
7.1 KiB
Go
263 lines
7.1 KiB
Go
// Package main provides an example of discovering SoundTouch devices using all three mechanisms.
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"flag"
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
"time"
|
|
|
|
"github.com/gesellix/bose-soundtouch/pkg/config"
|
|
"github.com/gesellix/bose-soundtouch/pkg/discovery"
|
|
)
|
|
|
|
func main() {
|
|
verbose := flag.Bool("verbose", false, "Enable verbose logging")
|
|
timeout := flag.Duration("timeout", 5*time.Second, "Discovery timeout")
|
|
showConfig := flag.Bool("show-config", false, "Show configuration details")
|
|
|
|
flag.Parse()
|
|
|
|
// Configure logging
|
|
if *verbose {
|
|
log.SetOutput(os.Stdout)
|
|
log.SetFlags(log.LstdFlags | log.Lmicroseconds)
|
|
} else {
|
|
log.SetOutput(os.Stderr)
|
|
}
|
|
|
|
fmt.Println("SoundTouch Unified Discovery Example")
|
|
fmt.Println("===================================")
|
|
|
|
fmt.Printf("Timeout: %v, Verbose: %v\n", *timeout, *verbose)
|
|
fmt.Println()
|
|
|
|
// Load configuration
|
|
cfg, err := config.LoadFromEnv()
|
|
if err != nil {
|
|
fmt.Printf("Failed to load configuration: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
// Override timeout from command line
|
|
cfg.DiscoveryTimeout = *timeout
|
|
|
|
if *showConfig {
|
|
printConfiguration(cfg)
|
|
}
|
|
|
|
fmt.Println("Testing individual discovery mechanisms:")
|
|
fmt.Println("--------------------------------------")
|
|
|
|
testSSDP(cfg, *timeout, *verbose)
|
|
testMDNS(cfg, *timeout, *verbose)
|
|
testConfig(cfg, *verbose)
|
|
testUnified(cfg, *timeout, *verbose)
|
|
}
|
|
|
|
func printConfiguration(cfg *config.Config) {
|
|
fmt.Println("Configuration:")
|
|
fmt.Printf(" UPnP Enabled: %v\n", cfg.UPnPEnabled)
|
|
fmt.Printf(" mDNS Enabled: %v\n", cfg.MDNSEnabled)
|
|
fmt.Printf(" Cache Enabled: %v\n", cfg.CacheEnabled)
|
|
fmt.Printf(" Discovery Timeout: %v\n", cfg.DiscoveryTimeout)
|
|
fmt.Printf(" Preferred Devices: %d\n", len(cfg.PreferredDevices))
|
|
|
|
for i, device := range cfg.PreferredDevices {
|
|
fmt.Printf(" %d. %s at %s:%d\n", i+1, device.Name, device.Host, device.Port)
|
|
}
|
|
|
|
fmt.Println()
|
|
}
|
|
|
|
func testSSDP(cfg *config.Config, timeout time.Duration, verbose bool) {
|
|
// Test SSDP discovery
|
|
fmt.Println("1. SSDP/UPnP Discovery:")
|
|
|
|
if cfg.UPnPEnabled {
|
|
// Create fresh context for SSDP test
|
|
ssdpCtx, ssdpCancel := context.WithTimeout(context.Background(), timeout+2*time.Second)
|
|
defer ssdpCancel()
|
|
|
|
ssdpService := discovery.NewServiceWithConfig(cfg)
|
|
start := time.Now()
|
|
ssdpDevices, ssdpErr := ssdpService.DiscoverDevices(ssdpCtx)
|
|
duration := time.Since(start)
|
|
|
|
if ssdpErr != nil {
|
|
fmt.Printf(" Error: %v\n", ssdpErr)
|
|
} else {
|
|
fmt.Printf(" Found %d devices in %v\n", len(ssdpDevices), duration)
|
|
|
|
for _, device := range ssdpDevices {
|
|
fmt.Printf(" - %s (%s:%d)\n", device.Name, device.Host, device.Port)
|
|
|
|
if verbose {
|
|
fmt.Printf(" Info URL: %s\n", device.InfoURL)
|
|
|
|
if device.UPnPLocation != "" {
|
|
fmt.Printf(" UPnP Location: %s\n", device.UPnPLocation)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
fmt.Println(" Disabled in configuration")
|
|
}
|
|
|
|
fmt.Println()
|
|
}
|
|
|
|
func testMDNS(cfg *config.Config, timeout time.Duration, verbose bool) {
|
|
// Test mDNS discovery
|
|
fmt.Println("2. mDNS/Bonjour Discovery:")
|
|
|
|
if cfg.MDNSEnabled {
|
|
// Create fresh context for mDNS test
|
|
mdnsCtx, mdnsCancel := context.WithTimeout(context.Background(), timeout+2*time.Second)
|
|
defer mdnsCancel()
|
|
|
|
mdnsService := discovery.NewMDNSDiscoveryService(timeout)
|
|
start := time.Now()
|
|
mdnsDevices, mdnsErr := mdnsService.DiscoverDevices(mdnsCtx)
|
|
duration := time.Since(start)
|
|
|
|
if mdnsErr != nil {
|
|
fmt.Printf(" Error: %v\n", mdnsErr)
|
|
} else {
|
|
fmt.Printf(" Found %d devices in %v\n", len(mdnsDevices), duration)
|
|
|
|
for _, device := range mdnsDevices {
|
|
fmt.Printf(" - %s (%s:%d)\n", device.Name, device.Host, device.Port)
|
|
|
|
if verbose {
|
|
fmt.Printf(" Info URL: %s\n", device.InfoURL)
|
|
|
|
if device.MDNSHostname != "" {
|
|
fmt.Printf(" mDNS Hostname: %s\n", device.MDNSHostname)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
fmt.Println(" Disabled in configuration")
|
|
}
|
|
|
|
fmt.Println()
|
|
}
|
|
|
|
func testConfig(cfg *config.Config, verbose bool) {
|
|
// Test configuration-based devices
|
|
fmt.Println("3. Configuration-based Devices:")
|
|
|
|
configDevices := cfg.GetPreferredDevicesAsDiscovered()
|
|
if len(configDevices) > 0 {
|
|
fmt.Printf(" Found %d configured devices\n", len(configDevices))
|
|
|
|
for _, device := range configDevices {
|
|
fmt.Printf(" - %s (%s:%d)\n", device.Name, device.Host, device.Port)
|
|
|
|
if verbose {
|
|
fmt.Printf(" Info URL: %s\n", device.InfoURL)
|
|
}
|
|
}
|
|
} else {
|
|
fmt.Println(" No devices configured in .env file")
|
|
}
|
|
|
|
fmt.Println()
|
|
}
|
|
|
|
func testUnified(cfg *config.Config, timeout time.Duration, verbose bool) {
|
|
// Test unified discovery
|
|
fmt.Println("4. Unified Discovery (combines all methods):")
|
|
// Create fresh context for unified test
|
|
unifiedCtx, unifiedCancel := context.WithTimeout(context.Background(), timeout+2*time.Second)
|
|
defer unifiedCancel()
|
|
|
|
unifiedService := discovery.NewUnifiedDiscoveryService(cfg)
|
|
start := time.Now()
|
|
allDevices, err := unifiedService.DiscoverDevices(unifiedCtx)
|
|
duration := time.Since(start)
|
|
|
|
if err != nil {
|
|
fmt.Printf(" Error: %v\n", err)
|
|
return
|
|
}
|
|
|
|
fmt.Printf(" Found %d total devices in %v\n", len(allDevices), duration)
|
|
fmt.Println()
|
|
|
|
if len(allDevices) == 0 {
|
|
fmt.Println("No SoundTouch devices found via any discovery method")
|
|
fmt.Println()
|
|
fmt.Println("This could mean:")
|
|
fmt.Println("- No SoundTouch devices on network")
|
|
fmt.Println("- All discovery methods are disabled")
|
|
fmt.Println("- Network blocks multicast traffic")
|
|
fmt.Println("- Devices are not advertising services")
|
|
|
|
return
|
|
}
|
|
|
|
fmt.Println("Unified Device List:")
|
|
fmt.Println("-------------------")
|
|
|
|
for i, device := range allDevices {
|
|
fmt.Printf("%d. %s\n", i+1, device.Name)
|
|
fmt.Printf(" Host: %s\n", device.Host)
|
|
fmt.Printf(" Port: %d\n", device.Port)
|
|
fmt.Printf(" API Base URL: %s\n", device.APIBaseURL)
|
|
fmt.Printf(" Info URL: %s\n", device.InfoURL)
|
|
fmt.Printf(" Discovery Method: %s\n", device.DiscoveryMethod)
|
|
fmt.Printf(" Last seen: %s\n", device.LastSeen.Format("2006-01-02 15:04:05"))
|
|
|
|
if verbose {
|
|
if device.ModelID != "" {
|
|
fmt.Printf(" Model ID: %s\n", device.ModelID)
|
|
}
|
|
|
|
if device.SerialNo != "" {
|
|
fmt.Printf(" Serial No: %s\n", device.SerialNo)
|
|
}
|
|
|
|
// Show protocol-specific details
|
|
if device.UPnPLocation != "" {
|
|
fmt.Printf(" UPnP Location: %s\n", device.UPnPLocation)
|
|
|
|
if device.UPnPUSN != "" {
|
|
fmt.Printf(" UPnP USN: %s\n", device.UPnPUSN)
|
|
}
|
|
}
|
|
|
|
if device.MDNSHostname != "" {
|
|
fmt.Printf(" mDNS Hostname: %s\n", device.MDNSHostname)
|
|
|
|
if device.MDNSService != "" {
|
|
fmt.Printf(" mDNS Service: %s\n", device.MDNSService)
|
|
}
|
|
}
|
|
|
|
if device.ConfigName != "" {
|
|
fmt.Printf(" Config Name: %s\n", device.ConfigName)
|
|
}
|
|
}
|
|
|
|
fmt.Println()
|
|
}
|
|
|
|
fmt.Printf("✓ Unified discovery completed successfully!\n")
|
|
fmt.Printf("✓ Found %d unique device(s) in %v\n", len(allDevices), duration)
|
|
|
|
if verbose {
|
|
fmt.Println()
|
|
fmt.Println("Technical Details:")
|
|
fmt.Printf("- SSDP multicast address: 239.255.255.250:1900\n")
|
|
fmt.Printf("- mDNS service type: _soundtouch._tcp.local\n")
|
|
fmt.Printf("- Discovery timeout: %v\n", timeout)
|
|
fmt.Printf("- Configuration file: .env (if present)\n")
|
|
}
|
|
}
|