mirror of
https://github.com/gesellix/Bose-SoundTouch.git
synced 2026-08-19 00:56:16 +00:00
Implement /name, /capabilities, and /presets informational endpoints
## New Endpoints ### GET /name ✅ - Simple device name retrieval with XML parsing - Helper methods for name validation and display - Real device name integration with anonymization ### GET /capabilities ✅ - Comprehensive device capabilities detection - Complex XML structure with nested network, DSP, and system configurations - Smart categorization: System Features, Audio Features, Network Features - Capability-specific helper methods (HasLRStereoCapability, HasDualModeNetwork, etc.) - Extended capabilities parsing with URLs and metadata ### GET /presets ✅ - Complete preset management with timestamps and metadata - Spotify playlist integration with anonymized account information - Smart filtering: by source, used/empty slots, most recent, oldest presets - Comprehensive analysis: preset summaries with source breakdowns - Time-based operations: creation/update timestamps with formatted display ## Device Introspection Features ### Capability Detection - System capabilities: Light Switch, Clock Display, BCO Reset, Power Saving - Audio capabilities: L/R Stereo support, DSP Mono/Stereo availability - Network capabilities: Dual Mode, WSAPI Proxy, Hosted WiFi Configuration - Extended capabilities: Custom endpoint discovery with URL mapping ### Preset Analysis - Usage pattern analysis (used vs empty slots) - Source distribution (Spotify, TuneIn, etc.) - Temporal analysis (most recent, oldest presets) - Content metadata extraction (artwork URLs, display names) ## Enhanced CLI Tool ### New Commands - Added -name command with simple device identification - Added -capabilities command with categorized feature display - Added -presets command with comprehensive preset analysis - Enhanced help system with all new command examples ### Rich Output Formatting - Capability categorization with bullet-point display - Preset timeline with creation/update timestamps - Smart metadata display (artwork, source accounts, content types) - Device-specific feature highlighting (different capabilities per device) ## Real Device Integration ### Multi-Device Testing - Device 192.168.178.28: SoundTouch 10 with Light Switch, Clock Display, Hosted WiFi - Device 192.168.178.35: SoundTouch 20 with L/R Stereo, Dual Mode networking - Verified capability differences between device models - Real preset data with anonymized Spotify account information ### Edge Case Handling - Non-responsive endpoints (/trackInfo timeout handling) - Empty preset configurations - Missing capability sections - Device-specific feature variations ## Quality & Testing ### Comprehensive Test Coverage - 15+ unit tests for XML models with real device response patterns - Client integration tests with mock HTTP servers - Edge case validation (empty names, missing capabilities, no presets) - Timestamp parsing and validation with Unix epoch conversion ### Production-Ready Features - Type-safe XML unmarshaling with custom validation - Robust error handling for network and parsing failures - Privacy protection with anonymized real device data - Documentation updates with real-world usage examples ## API Coverage Progress ✅ Complete Information Endpoints: - GET /info - Device information - GET /name - Device name - GET /capabilities - Device capabilities - GET /presets - Configured presets - GET /now_playing - Current playback status - GET /sources - Available audio sources 🔄 Next Phase - Control Endpoints: - POST /key - Media controls - GET/POST /volume - Volume management - WebSocket / - Real-time events Features: ✅ Comprehensive device introspection and capability detection ✅ Smart preset management with timeline analysis ✅ Multi-device support with hardware-specific feature detection ✅ Production-ready error handling and data validation ✅ Rich CLI interface with categorized output formatting ✅ Real device integration with privacy-protected test data
This commit is contained in:
+265
-10
@@ -16,15 +16,18 @@ import (
|
||||
|
||||
func main() {
|
||||
var (
|
||||
host = flag.String("host", "", "SoundTouch device host/IP address")
|
||||
port = flag.Int("port", 8090, "SoundTouch device port")
|
||||
timeout = flag.Duration("timeout", 10*time.Second, "Request timeout")
|
||||
discover = flag.Bool("discover", false, "Discover SoundTouch devices via UPnP")
|
||||
discoverAll = flag.Bool("discover-all", false, "Discover all SoundTouch devices and show info")
|
||||
info = flag.Bool("info", false, "Get device information")
|
||||
nowPlaying = flag.Bool("nowplaying", false, "Get current playback status")
|
||||
sources = flag.Bool("sources", false, "Get available audio sources")
|
||||
help = flag.Bool("help", false, "Show help")
|
||||
host = flag.String("host", "", "SoundTouch device host/IP address")
|
||||
port = flag.Int("port", 8090, "SoundTouch device port")
|
||||
timeout = flag.Duration("timeout", 10*time.Second, "Request timeout")
|
||||
discover = flag.Bool("discover", false, "Discover SoundTouch devices via UPnP")
|
||||
discoverAll = flag.Bool("discover-all", false, "Discover all SoundTouch devices and show info")
|
||||
info = flag.Bool("info", false, "Get device information")
|
||||
nowPlaying = flag.Bool("nowplaying", false, "Get current playback status")
|
||||
sources = flag.Bool("sources", false, "Get available audio sources")
|
||||
name = flag.Bool("name", false, "Get device name")
|
||||
capabilities = flag.Bool("capabilities", false, "Get device capabilities")
|
||||
presets = flag.Bool("presets", false, "Get configured presets")
|
||||
help = flag.Bool("help", false, "Show help")
|
||||
)
|
||||
|
||||
flag.Parse()
|
||||
@@ -35,7 +38,7 @@ func main() {
|
||||
}
|
||||
|
||||
// If no specific action is requested, show help
|
||||
if !*discover && !*discoverAll && !*info && !*nowPlaying && !*sources && *host == "" {
|
||||
if !*discover && !*discoverAll && !*info && !*nowPlaying && !*sources && !*name && !*capabilities && !*presets && *host == "" {
|
||||
printHelp()
|
||||
return
|
||||
}
|
||||
@@ -80,6 +83,39 @@ func main() {
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Handle name
|
||||
if *name {
|
||||
if *host == "" {
|
||||
log.Fatal("Host is required for name command. Use -host flag or -discover to find devices.")
|
||||
}
|
||||
if err := handleName(*host, *port, *timeout); err != nil {
|
||||
log.Fatalf("Failed to get device name: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Handle capabilities
|
||||
if *capabilities {
|
||||
if *host == "" {
|
||||
log.Fatal("Host is required for capabilities command. Use -host flag or -discover to find devices.")
|
||||
}
|
||||
if err := handleCapabilities(*host, *port, *timeout); err != nil {
|
||||
log.Fatalf("Failed to get device capabilities: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Handle presets
|
||||
if *presets {
|
||||
if *host == "" {
|
||||
log.Fatal("Host is required for presets command. Use -host flag or -discover to find devices.")
|
||||
}
|
||||
if err := handlePresets(*host, *port, *timeout); err != nil {
|
||||
log.Fatalf("Failed to get presets: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func printHelp() {
|
||||
@@ -97,6 +133,9 @@ func printHelp() {
|
||||
fmt.Println(" -info Get device information (requires -host)")
|
||||
fmt.Println(" -nowplaying Get current playback status (requires -host)")
|
||||
fmt.Println(" -sources Get available audio sources (requires -host)")
|
||||
fmt.Println(" -name Get device name (requires -host)")
|
||||
fmt.Println(" -capabilities Get device capabilities (requires -host)")
|
||||
fmt.Println(" -presets Get configured presets (requires -host)")
|
||||
fmt.Println(" -help Show this help message")
|
||||
fmt.Println()
|
||||
fmt.Println("Examples:")
|
||||
@@ -105,6 +144,9 @@ func printHelp() {
|
||||
fmt.Println(" soundtouch-cli -host 192.168.1.100 -info")
|
||||
fmt.Println(" soundtouch-cli -host 192.168.1.100 -nowplaying")
|
||||
fmt.Println(" soundtouch-cli -host 192.168.1.100 -sources")
|
||||
fmt.Println(" soundtouch-cli -host 192.168.1.100 -name")
|
||||
fmt.Println(" soundtouch-cli -host 192.168.1.100 -capabilities")
|
||||
fmt.Println(" soundtouch-cli -host 192.168.1.100 -presets")
|
||||
fmt.Println(" soundtouch-cli -host 192.168.1.100 -port 8090 -info")
|
||||
}
|
||||
|
||||
@@ -469,3 +511,216 @@ func handleSources(host string, port int, timeout time.Duration) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleName(host string, port int, timeout time.Duration) error {
|
||||
cfg, err := config.LoadFromEnv()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load config: %w", err)
|
||||
}
|
||||
|
||||
// Override config with command line arguments if provided
|
||||
if timeout > 0 {
|
||||
cfg.HTTPTimeout = timeout
|
||||
}
|
||||
|
||||
clientConfig := client.ClientConfig{
|
||||
Host: host,
|
||||
Port: port,
|
||||
Timeout: cfg.HTTPTimeout,
|
||||
UserAgent: cfg.UserAgent,
|
||||
}
|
||||
|
||||
soundtouchClient := client.NewClient(clientConfig)
|
||||
|
||||
fmt.Printf("Getting device name from %s:%d...\n", host, port)
|
||||
|
||||
// Get device name
|
||||
name, err := soundtouchClient.GetName()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get device name: %w", err)
|
||||
}
|
||||
|
||||
// Display name information
|
||||
fmt.Printf("Device Name: %s\n", name.GetName())
|
||||
|
||||
if name.IsEmpty() {
|
||||
fmt.Printf("Warning: Device name is empty\n")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleCapabilities(host string, port int, timeout time.Duration) error {
|
||||
cfg, err := config.LoadFromEnv()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load config: %w", err)
|
||||
}
|
||||
|
||||
// Override config with command line arguments if provided
|
||||
if timeout > 0 {
|
||||
cfg.HTTPTimeout = timeout
|
||||
}
|
||||
|
||||
clientConfig := client.ClientConfig{
|
||||
Host: host,
|
||||
Port: port,
|
||||
Timeout: cfg.HTTPTimeout,
|
||||
UserAgent: cfg.UserAgent,
|
||||
}
|
||||
|
||||
soundtouchClient := client.NewClient(clientConfig)
|
||||
|
||||
fmt.Printf("Getting device capabilities from %s:%d...\n", host, port)
|
||||
|
||||
// Get device capabilities
|
||||
capabilities, err := soundtouchClient.GetCapabilities()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get device capabilities: %w", err)
|
||||
}
|
||||
|
||||
// Display capabilities information
|
||||
fmt.Printf("Device Capabilities:\n")
|
||||
fmt.Printf(" Device ID: %s\n", capabilities.DeviceID)
|
||||
fmt.Println()
|
||||
|
||||
// System capabilities
|
||||
systemCaps := capabilities.GetSystemCapabilities()
|
||||
if len(systemCaps) > 0 {
|
||||
fmt.Printf("System Features:\n")
|
||||
for _, cap := range systemCaps {
|
||||
fmt.Printf(" • %s\n", cap)
|
||||
}
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
// Audio capabilities
|
||||
audioCaps := capabilities.GetAudioCapabilities()
|
||||
if len(audioCaps) > 0 {
|
||||
fmt.Printf("Audio Features:\n")
|
||||
for _, cap := range audioCaps {
|
||||
fmt.Printf(" • %s\n", cap)
|
||||
}
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
// Network capabilities
|
||||
networkCaps := capabilities.GetNetworkCapabilities()
|
||||
if len(networkCaps) > 0 {
|
||||
fmt.Printf("Network Features:\n")
|
||||
for _, cap := range networkCaps {
|
||||
fmt.Printf(" • %s\n", cap)
|
||||
}
|
||||
|
||||
// Show hosted wifi details if available
|
||||
if capabilities.HasHostedWifiConfig() {
|
||||
fmt.Printf(" Hosted WiFi Config:\n")
|
||||
fmt.Printf(" • Port: %s\n", capabilities.GetHostedWifiPort())
|
||||
fmt.Printf(" • Hosted by: %s\n", capabilities.GetHostedWifiHostedBy())
|
||||
}
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
// Extended capabilities
|
||||
capNames := capabilities.GetCapabilityNames()
|
||||
if len(capNames) > 0 {
|
||||
fmt.Printf("Extended Capabilities:\n")
|
||||
for _, capName := range capNames {
|
||||
cap := capabilities.GetCapabilityByName(capName)
|
||||
fmt.Printf(" • %s", capName)
|
||||
if cap.URL != "" {
|
||||
fmt.Printf(" (%s)", cap.URL)
|
||||
}
|
||||
fmt.Println()
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func handlePresets(host string, port int, timeout time.Duration) error {
|
||||
cfg, err := config.LoadFromEnv()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load config: %w", err)
|
||||
}
|
||||
|
||||
// Override config with command line arguments if provided
|
||||
if timeout > 0 {
|
||||
cfg.HTTPTimeout = timeout
|
||||
}
|
||||
|
||||
clientConfig := client.ClientConfig{
|
||||
Host: host,
|
||||
Port: port,
|
||||
Timeout: cfg.HTTPTimeout,
|
||||
UserAgent: cfg.UserAgent,
|
||||
}
|
||||
|
||||
soundtouchClient := client.NewClient(clientConfig)
|
||||
|
||||
fmt.Printf("Getting configured presets from %s:%d...\n", host, port)
|
||||
|
||||
// Get presets
|
||||
presets, err := soundtouchClient.GetPresets()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get presets: %w", err)
|
||||
}
|
||||
|
||||
// Display presets information
|
||||
fmt.Printf("Configured Presets:\n")
|
||||
|
||||
if !presets.HasPresets() {
|
||||
fmt.Printf(" No presets configured\n")
|
||||
return nil
|
||||
}
|
||||
|
||||
summary := presets.GetPresetsSummary()
|
||||
fmt.Printf(" Used Slots: %d/6\n", summary["used"])
|
||||
fmt.Printf(" Spotify Presets: %d\n", summary["spotify"])
|
||||
fmt.Println()
|
||||
|
||||
// Show each configured preset
|
||||
for _, preset := range presets.Preset {
|
||||
if preset.IsEmpty() {
|
||||
continue
|
||||
}
|
||||
|
||||
fmt.Printf("Preset %d: %s\n", preset.ID, preset.GetDisplayName())
|
||||
fmt.Printf(" Source: %s", preset.GetSource())
|
||||
if preset.GetSourceAccount() != "" {
|
||||
fmt.Printf(" (%s)", preset.GetSourceAccount())
|
||||
}
|
||||
fmt.Println()
|
||||
|
||||
if preset.GetContentType() != "" {
|
||||
fmt.Printf(" Type: %s\n", preset.GetContentType())
|
||||
}
|
||||
|
||||
if preset.HasTimestamps() {
|
||||
if !preset.GetCreatedTime().IsZero() {
|
||||
fmt.Printf(" Created: %s\n", preset.GetCreatedTime().Format("2006-01-02 15:04:05"))
|
||||
}
|
||||
if !preset.GetUpdatedTime().IsZero() {
|
||||
fmt.Printf(" Updated: %s\n", preset.GetUpdatedTime().Format("2006-01-02 15:04:05"))
|
||||
}
|
||||
}
|
||||
|
||||
if preset.GetArtworkURL() != "" {
|
||||
fmt.Printf(" Artwork: %s\n", preset.GetArtworkURL())
|
||||
}
|
||||
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
// Show empty slots
|
||||
emptySlots := presets.GetEmptyPresetSlots()
|
||||
if len(emptySlots) > 0 {
|
||||
fmt.Printf("Available Slots: %v\n", emptySlots)
|
||||
}
|
||||
|
||||
// Show most recent preset
|
||||
if recent := presets.GetMostRecentPreset(); recent != nil {
|
||||
fmt.Printf("Most Recent: Preset %d (%s)\n", recent.ID, recent.GetDisplayName())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user