Files
Bose-SoundTouch/pkg/models/presets.go
T
Tobias Gesellchen de2ff3550f 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
2026-01-08 23:32:18 +01:00

245 lines
5.7 KiB
Go

package models
import (
"encoding/xml"
"strconv"
"time"
)
// Presets represents the response from /presets endpoint
type Presets struct {
XMLName xml.Name `xml:"presets"`
Preset []Preset `xml:"preset"`
}
// Preset represents an individual preset
type Preset struct {
ID int `xml:"id,attr"`
CreatedOn *int64 `xml:"createdOn,attr,omitempty"`
UpdatedOn *int64 `xml:"updatedOn,attr,omitempty"`
ContentItem *ContentItem `xml:"ContentItem,omitempty"`
}
// GetCreatedTime returns the creation time as a time.Time
func (p *Preset) GetCreatedTime() time.Time {
if p.CreatedOn != nil {
return time.Unix(*p.CreatedOn, 0)
}
return time.Time{}
}
// GetUpdatedTime returns the last updated time as a time.Time
func (p *Preset) GetUpdatedTime() time.Time {
if p.UpdatedOn != nil {
return time.Unix(*p.UpdatedOn, 0)
}
return time.Time{}
}
// HasTimestamps returns true if the preset has creation/update timestamps
func (p *Preset) HasTimestamps() bool {
return p.CreatedOn != nil || p.UpdatedOn != nil
}
// GetDisplayName returns the best available display name for the preset
func (p *Preset) GetDisplayName() string {
if p.ContentItem != nil && p.ContentItem.ItemName != "" {
return p.ContentItem.ItemName
}
return "Preset " + strconv.Itoa(p.ID)
}
// GetArtworkURL returns the artwork URL if available
func (p *Preset) GetArtworkURL() string {
if p.ContentItem != nil && p.ContentItem.ContainerArt != "" {
return p.ContentItem.ContainerArt
}
return ""
}
// IsSpotifyPreset returns true if this is a Spotify preset
func (p *Preset) IsSpotifyPreset() bool {
return p.ContentItem != nil && p.ContentItem.Source == "SPOTIFY"
}
// IsEmpty returns true if the preset has no content
func (p *Preset) IsEmpty() bool {
return p.ContentItem == nil
}
// GetSource returns the source of the preset content
func (p *Preset) GetSource() string {
if p.ContentItem != nil {
return p.ContentItem.Source
}
return ""
}
// GetSourceAccount returns the source account of the preset content
func (p *Preset) GetSourceAccount() string {
if p.ContentItem != nil {
return p.ContentItem.SourceAccount
}
return ""
}
// GetContentType returns the content type of the preset
func (p *Preset) GetContentType() string {
if p.ContentItem != nil {
return p.ContentItem.Type
}
return ""
}
// GetLocation returns the content location/URL
func (p *Preset) GetLocation() string {
if p.ContentItem != nil {
return p.ContentItem.Location
}
return ""
}
// IsPresetable returns true if the content can be saved as a preset
func (p *Preset) IsPresetable() bool {
return p.ContentItem != nil && p.ContentItem.IsPresetable
}
// GetPresetCount returns the total number of presets
func (ps *Presets) GetPresetCount() int {
return len(ps.Preset)
}
// GetPresetByID returns a preset by its ID
func (ps *Presets) GetPresetByID(id int) *Preset {
for _, preset := range ps.Preset {
if preset.ID == id {
return &preset
}
}
return nil
}
// GetSpotifyPresets returns all Spotify presets
func (ps *Presets) GetSpotifyPresets() []Preset {
var spotify []Preset
for _, preset := range ps.Preset {
if preset.IsSpotifyPreset() {
spotify = append(spotify, preset)
}
}
return spotify
}
// GetPresetsBySource returns presets filtered by source
func (ps *Presets) GetPresetsBySource(source string) []Preset {
var filtered []Preset
for _, preset := range ps.Preset {
if preset.GetSource() == source {
filtered = append(filtered, preset)
}
}
return filtered
}
// GetEmptyPresetSlots returns preset IDs that are empty (1-6)
func (ps *Presets) GetEmptyPresetSlots() []int {
var empty []int
used := make(map[int]bool)
// Mark used slots
for _, preset := range ps.Preset {
if !preset.IsEmpty() {
used[preset.ID] = true
}
}
// Find empty slots (1-6 are typical preset slots)
for i := 1; i <= 6; i++ {
if !used[i] {
empty = append(empty, i)
}
}
return empty
}
// HasPresets returns true if there are any presets configured
func (ps *Presets) HasPresets() bool {
for _, preset := range ps.Preset {
if !preset.IsEmpty() {
return true
}
}
return false
}
// GetUsedPresetSlots returns preset IDs that have content
func (ps *Presets) GetUsedPresetSlots() []int {
var used []int
for _, preset := range ps.Preset {
if !preset.IsEmpty() {
used = append(used, preset.ID)
}
}
return used
}
// GetMostRecentPreset returns the most recently updated preset
func (ps *Presets) GetMostRecentPreset() *Preset {
var mostRecent *Preset
var latestTime int64
for _, preset := range ps.Preset {
if preset.UpdatedOn != nil && *preset.UpdatedOn > latestTime {
latestTime = *preset.UpdatedOn
mostRecent = &preset
} else if preset.CreatedOn != nil && preset.UpdatedOn == nil && *preset.CreatedOn > latestTime {
latestTime = *preset.CreatedOn
mostRecent = &preset
}
}
return mostRecent
}
// GetOldestPreset returns the oldest preset
func (ps *Presets) GetOldestPreset() *Preset {
var oldest *Preset
var earliestTime int64 = 9223372036854775807 // max int64
for _, preset := range ps.Preset {
if preset.CreatedOn != nil && *preset.CreatedOn < earliestTime {
earliestTime = *preset.CreatedOn
oldest = &preset
}
}
return oldest
}
// GetPresetsSummary returns a summary of preset usage
func (ps *Presets) GetPresetsSummary() map[string]int {
summary := map[string]int{
"total": ps.GetPresetCount(),
"used": len(ps.GetUsedPresetSlots()),
"empty": len(ps.GetEmptyPresetSlots()),
"spotify": len(ps.GetSpotifyPresets()),
}
// Count by source
sources := make(map[string]int)
for _, preset := range ps.Preset {
if !preset.IsEmpty() {
source := preset.GetSource()
sources[source]++
}
}
// Add source counts to summary
for source, count := range sources {
summary[source] = count
}
return summary
}