// Package models provides data structures and types for the Bose SoundTouch API. package models import ( "encoding/xml" "fmt" ) // Balance represents the response from /balance endpoint type Balance struct { XMLName xml.Name `xml:"balance"` DeviceID string `xml:"deviceID,attr"` TargetBalance int `xml:"targetbalance"` ActualBalance int `xml:"actualbalance"` } // BalanceRequest represents the request for POST /balance endpoint type BalanceRequest struct { XMLName xml.Name `xml:"balance"` Level int `xml:",chardata"` } // Balance level constants const ( BalanceLevelMin = -50 BalanceLevelMax = 50 BalanceLevelDefault = 0 ) // NewBalanceRequest creates a new balance request with validation func NewBalanceRequest(level int) (*BalanceRequest, error) { if !ValidateBalanceLevel(level) { return nil, fmt.Errorf("invalid balance level: %d (must be between %d and %d)", level, BalanceLevelMin, BalanceLevelMax) } return &BalanceRequest{ Level: level, }, nil } // ValidateBalanceLevel validates that a balance level is within the allowed range func ValidateBalanceLevel(level int) bool { return level >= BalanceLevelMin && level <= BalanceLevelMax } // ClampBalanceLevel clamps a balance level to the valid range func ClampBalanceLevel(level int) int { if level < BalanceLevelMin { return BalanceLevelMin } if level > BalanceLevelMax { return BalanceLevelMax } return level } // GetLevel returns the target balance level func (b *Balance) GetLevel() int { return b.TargetBalance } // GetActualLevel returns the actual balance level func (b *Balance) GetActualLevel() int { return b.ActualBalance } // IsAtTarget returns true if actual balance matches target balance func (b *Balance) IsAtTarget() bool { return b.TargetBalance == b.ActualBalance } // GetBalanceLevelName returns a descriptive name for the balance level func GetBalanceLevelName(level int) string { switch { case level < -30: return "Far Left" case level < -10: return "Left" case level < 0: return "Slightly Left" case level == 0: return "Center" case level <= 10: return "Slightly Right" case level <= 30: return "Right" default: return "Far Right" } } // GetBalanceLevelCategory returns the balance category func GetBalanceLevelCategory(level int) string { switch { case level < 0: return "Left Channel" case level == 0: return "Balanced" default: return "Right Channel" } } // String returns a human-readable string representation func (b *Balance) String() string { return fmt.Sprintf("Balance: %d (%s)", b.GetLevel(), GetBalanceLevelName(b.GetLevel())) } // UnmarshalXML implements custom XML unmarshaling with validation func (b *Balance) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { // Use a temporary struct to avoid infinite recursion type TempBalance Balance temp := (*TempBalance)(b) if err := d.DecodeElement(temp, &start); err != nil { return err } // Validate balance levels are within acceptable range if !ValidateBalanceLevel(b.TargetBalance) { return fmt.Errorf("invalid target balance level: %d", b.TargetBalance) } if !ValidateBalanceLevel(b.ActualBalance) { return fmt.Errorf("invalid actual balance level: %d", b.ActualBalance) } return nil } // MarshalXML implements custom XML marshaling func (b *Balance) MarshalXML(e *xml.Encoder, start xml.StartElement) error { type TempBalance Balance temp := (*TempBalance)(b) return e.EncodeElement(temp, start) } // IsLeftBalance returns true if balance favors left channel (negative level) func (b *Balance) IsLeftBalance() bool { return b.GetLevel() < 0 } // IsRightBalance returns true if balance favors right channel (positive level) func (b *Balance) IsRightBalance() bool { return b.GetLevel() > 0 } // IsBalanced returns true if balance is centered (zero level) func (b *Balance) IsBalanced() bool { return b.GetLevel() == 0 } // GetBalanceChangeNeeded returns the amount of change needed to reach target from actual func (b *Balance) GetBalanceChangeNeeded() int { return b.TargetBalance - b.ActualBalance } // GetLeftRightPercentage returns the balance as left/right percentages func (b *Balance) GetLeftRightPercentage() (left, right int) { level := b.GetLevel() if level <= 0 { // Left emphasis or center left = 50 + (-level / 2) right = 50 - (-level / 2) } else { // Right emphasis left = 50 - (level / 2) right = 50 + (level / 2) } return left, right }