mirror of
https://github.com/gesellix/Bose-SoundTouch.git
synced 2026-08-10 04:36:14 +00:00
Brings the five remaining gocyclo > 20 warnings to zero by extracting cohesive sub-functions; same observable behaviour, smaller surface to read at each call site. Bonus: the new helpers are individually testable. - pkg/models/clockdisplay.go: split ClockDisplay.UnmarshalXML attr handling into applyClockDisplayOuterAttrs (legacy flat shape) and applyClockConfigAttrs (current nested shape). - pkg/service/setup/ssh_probe_apply.go: split applyProbeToSummary into applyProbeCurrentConfig / applyProbeResolvConf / applyProbeRemoteServices / applyProbeCACert — one helper per MigrationSummary axis the probe populates. - pkg/service/setup/init_plan.go: split ExecuteInitPlan into applyInitPlanDefaults, runURLRewrite, resolveAccountID, and verifyPairing. Cleans up several shadowed err variables in the process. - cmd/soundtouch-cli/cmd_setup.go: split renderInspectReport into renderInspectIdentityAndPairing / renderInspectNetwork / renderInspectSources / renderInspectPresets / renderInspectRuntimeURLs, and buildPlanSteps into resetSteps + migrationSteps helpers. golangci-lint run ./pkg/service/setup/... ./pkg/models/... ./cmd/soundtouch-cli/... now reports zero findings. Tests green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
383 lines
9.6 KiB
Go
383 lines
9.6 KiB
Go
package models
|
|
|
|
import (
|
|
"encoding/xml"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
// ClockDisplay represents the device's clock display settings.
|
|
//
|
|
// Wire format (confirmed against ST10/ST20 firmware 27.0.6 — flat
|
|
// attributes on the outer <clockDisplay> are rejected with
|
|
// "Error parsing request"):
|
|
//
|
|
// <clockDisplay deviceID="…">
|
|
// <clockConfig timezoneInfo="Europe/Berlin"
|
|
// userEnable="true"
|
|
// timeFormat="TIME_FORMAT_24HOUR_ID"
|
|
// userOffsetMinute="0"
|
|
// brightnessLevel="70"
|
|
// userUtcTime="0"/>
|
|
// </clockDisplay>
|
|
//
|
|
// The struct keeps its historical flat-field public API so the CLI and
|
|
// other callers don't have to be rewritten; custom MarshalXML /
|
|
// UnmarshalXML methods bridge to the nested format on the wire.
|
|
type ClockDisplay struct {
|
|
XMLName xml.Name `xml:"clockDisplay"`
|
|
DeviceID string
|
|
Enabled bool
|
|
Format string // public-facing values: "12", "24", "auto"
|
|
Brightness int
|
|
AutoDim bool // not on the device's wire format; preserved for API compat
|
|
TimeZone string
|
|
Value string // kept for API compat — older fixtures stored chardata here
|
|
}
|
|
|
|
// Wire constants for clockConfig/@timeFormat.
|
|
const (
|
|
wireTimeFormat12Hour = "TIME_FORMAT_12HOUR_ID"
|
|
wireTimeFormat24Hour = "TIME_FORMAT_24HOUR_ID"
|
|
wireTimeFormatAuto = "TIME_FORMAT_AUTO_ID"
|
|
)
|
|
|
|
func mapToWireFormat(f string) string {
|
|
switch strings.ToLower(f) {
|
|
case "12":
|
|
return wireTimeFormat12Hour
|
|
case "24":
|
|
return wireTimeFormat24Hour
|
|
case "auto":
|
|
return wireTimeFormatAuto
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
func mapFromWireFormat(wire string) string {
|
|
switch wire {
|
|
case wireTimeFormat12Hour:
|
|
return "12"
|
|
case wireTimeFormat24Hour:
|
|
return "24"
|
|
case wireTimeFormatAuto:
|
|
return "auto"
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
// UnmarshalXML decodes the nested <clockDisplay><clockConfig …/></clockDisplay>
|
|
// into ClockDisplay's flat fields. Tolerates the older flat shape too —
|
|
// either because it appears in legacy captures or for forward-compat with
|
|
// firmwares that may revert.
|
|
func (c *ClockDisplay) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
|
applyClockDisplayOuterAttrs(c, start.Attr)
|
|
|
|
for {
|
|
tok, err := d.Token()
|
|
if errors.Is(err, io.EOF) {
|
|
break
|
|
}
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
switch t := tok.(type) {
|
|
case xml.StartElement:
|
|
if t.Name.Local == "clockConfig" {
|
|
applyClockConfigAttrs(c, t.Attr)
|
|
}
|
|
|
|
if err := d.Skip(); err != nil {
|
|
return err
|
|
}
|
|
|
|
case xml.CharData:
|
|
if text := strings.TrimSpace(string(t)); text != "" {
|
|
c.Value = text
|
|
}
|
|
|
|
case xml.EndElement:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// applyClockDisplayOuterAttrs handles the legacy flat-attribute format
|
|
// (deviceID, enabled, format, brightness, autoDim, timeZone) that older
|
|
// fixtures used directly on the <clockDisplay> element.
|
|
func applyClockDisplayOuterAttrs(c *ClockDisplay, attrs []xml.Attr) {
|
|
for _, attr := range attrs {
|
|
switch attr.Name.Local {
|
|
case "deviceID":
|
|
c.DeviceID = attr.Value
|
|
case "enabled":
|
|
c.Enabled = attr.Value == "true"
|
|
case "format":
|
|
c.Format = attr.Value
|
|
case "brightness":
|
|
c.Brightness, _ = strconv.Atoi(attr.Value)
|
|
case "autoDim":
|
|
c.AutoDim = attr.Value == "true"
|
|
case "timeZone":
|
|
c.TimeZone = attr.Value
|
|
}
|
|
}
|
|
}
|
|
|
|
// applyClockConfigAttrs handles the nested <clockConfig> attributes
|
|
// (timezoneInfo, userEnable, timeFormat, brightnessLevel) — the shape
|
|
// FW 27 emits and accepts.
|
|
func applyClockConfigAttrs(c *ClockDisplay, attrs []xml.Attr) {
|
|
for _, attr := range attrs {
|
|
switch attr.Name.Local {
|
|
case "timezoneInfo":
|
|
c.TimeZone = attr.Value
|
|
case "userEnable":
|
|
c.Enabled = attr.Value == "true"
|
|
case "timeFormat":
|
|
if mapped := mapFromWireFormat(attr.Value); mapped != "" {
|
|
c.Format = mapped
|
|
}
|
|
case "brightnessLevel":
|
|
c.Brightness, _ = strconv.Atoi(attr.Value)
|
|
}
|
|
}
|
|
}
|
|
|
|
// ClockFormat represents supported clock display formats
|
|
type ClockFormat string
|
|
|
|
const (
|
|
// ClockFormat12Hour represents 12-hour clock format
|
|
ClockFormat12Hour ClockFormat = "12"
|
|
// ClockFormat24Hour represents 24-hour clock format
|
|
ClockFormat24Hour ClockFormat = "24"
|
|
// ClockFormatAuto represents automatic clock format selection
|
|
ClockFormatAuto ClockFormat = "auto"
|
|
)
|
|
|
|
// IsEnabled returns true if the clock display is enabled
|
|
func (c *ClockDisplay) IsEnabled() bool {
|
|
return c.Enabled
|
|
}
|
|
|
|
// GetFormat returns the clock display format (12/24 hour)
|
|
func (c *ClockDisplay) GetFormat() string {
|
|
if c.Format == "" {
|
|
return "12" // Default to 12-hour format
|
|
}
|
|
|
|
return c.Format
|
|
}
|
|
|
|
// GetFormatDescription returns a human-readable format description
|
|
func (c *ClockDisplay) GetFormatDescription() string {
|
|
switch strings.ToLower(c.Format) {
|
|
case "12":
|
|
return "12-hour format (AM/PM)"
|
|
case "24":
|
|
return "24-hour format"
|
|
case "auto":
|
|
return "Auto format (system default)"
|
|
default:
|
|
return "12-hour format (AM/PM)" // Default
|
|
}
|
|
}
|
|
|
|
// GetBrightness returns the display brightness level (0-100)
|
|
func (c *ClockDisplay) GetBrightness() int {
|
|
if c.Brightness < 0 {
|
|
return 0
|
|
}
|
|
|
|
if c.Brightness > 100 {
|
|
return 100
|
|
}
|
|
|
|
return c.Brightness
|
|
}
|
|
|
|
// GetBrightnessLevel returns a descriptive brightness level
|
|
func (c *ClockDisplay) GetBrightnessLevel() string {
|
|
brightness := c.GetBrightness()
|
|
switch {
|
|
case brightness == 0:
|
|
return "Off"
|
|
case brightness <= 25:
|
|
return "Low"
|
|
case brightness <= 50:
|
|
return "Medium"
|
|
case brightness <= 75:
|
|
return "High"
|
|
default:
|
|
return "Maximum"
|
|
}
|
|
}
|
|
|
|
// IsAutoDimEnabled returns true if auto-dim is enabled
|
|
func (c *ClockDisplay) IsAutoDimEnabled() bool {
|
|
return c.AutoDim
|
|
}
|
|
|
|
// GetTimeZone returns the timezone setting
|
|
func (c *ClockDisplay) GetTimeZone() string {
|
|
return c.TimeZone
|
|
}
|
|
|
|
// GetDeviceID returns the device ID
|
|
func (c *ClockDisplay) GetDeviceID() string {
|
|
return c.DeviceID
|
|
}
|
|
|
|
// IsEmpty returns true if the clock display has no configuration
|
|
func (c *ClockDisplay) IsEmpty() bool {
|
|
return !c.Enabled && c.Format == "" && c.Brightness == 0 && c.TimeZone == ""
|
|
}
|
|
|
|
// ClockDisplayRequest represents a request to configure clock display
|
|
// settings. Fields use the same public names as the response struct;
|
|
// MarshalXML produces the nested wire format the device requires.
|
|
type ClockDisplayRequest struct {
|
|
XMLName xml.Name `xml:"clockDisplay"`
|
|
Enabled *bool
|
|
Format string
|
|
Brightness *int
|
|
AutoDim *bool
|
|
TimeZone string
|
|
}
|
|
|
|
// NewClockDisplayRequest creates a new clock display configuration request
|
|
func NewClockDisplayRequest() *ClockDisplayRequest {
|
|
return &ClockDisplayRequest{}
|
|
}
|
|
|
|
// SetEnabled sets whether the clock display is enabled
|
|
func (r *ClockDisplayRequest) SetEnabled(enabled bool) *ClockDisplayRequest {
|
|
r.Enabled = &enabled
|
|
return r
|
|
}
|
|
|
|
// SetFormat sets the clock display format (12/24 hour)
|
|
func (r *ClockDisplayRequest) SetFormat(format ClockFormat) *ClockDisplayRequest {
|
|
r.Format = string(format)
|
|
return r
|
|
}
|
|
|
|
// SetBrightness sets the display brightness (0-100)
|
|
func (r *ClockDisplayRequest) SetBrightness(brightness int) *ClockDisplayRequest {
|
|
if brightness < 0 {
|
|
brightness = 0
|
|
}
|
|
|
|
if brightness > 100 {
|
|
brightness = 100
|
|
}
|
|
|
|
r.Brightness = &brightness
|
|
|
|
return r
|
|
}
|
|
|
|
// SetAutoDim sets whether auto-dim is enabled
|
|
func (r *ClockDisplayRequest) SetAutoDim(autoDim bool) *ClockDisplayRequest {
|
|
r.AutoDim = &autoDim
|
|
return r
|
|
}
|
|
|
|
// SetTimeZone sets the timezone
|
|
func (r *ClockDisplayRequest) SetTimeZone(timeZone string) *ClockDisplayRequest {
|
|
r.TimeZone = timeZone
|
|
return r
|
|
}
|
|
|
|
// Validate checks if the clock display request is valid
|
|
func (r *ClockDisplayRequest) Validate() error {
|
|
if r.Format != "" {
|
|
format := strings.ToLower(r.Format)
|
|
if format != "12" && format != "24" && format != "auto" {
|
|
return fmt.Errorf("invalid format '%s': must be '12', '24', or 'auto'", r.Format)
|
|
}
|
|
}
|
|
|
|
if r.Brightness != nil {
|
|
if *r.Brightness < 0 || *r.Brightness > 100 {
|
|
return fmt.Errorf("brightness must be between 0 and 100, got %d", *r.Brightness)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// HasChanges returns true if the request has any configuration changes
|
|
func (r *ClockDisplayRequest) HasChanges() bool {
|
|
return r.Enabled != nil || r.Format != "" || r.Brightness != nil || r.AutoDim != nil || r.TimeZone != ""
|
|
}
|
|
|
|
// MarshalXML emits the nested <clockDisplay><clockConfig …/></clockDisplay>
|
|
// envelope the device accepts. Empty fields are omitted so partial updates
|
|
// (e.g. "set only the timezone") don't accidentally clear other settings.
|
|
//
|
|
// AutoDim has no counterpart in the captured wire format; we still accept
|
|
// it in the public API for backward-compat but it is not emitted.
|
|
func (r ClockDisplayRequest) MarshalXML(e *xml.Encoder, _ xml.StartElement) error {
|
|
display := xml.StartElement{Name: xml.Name{Local: "clockDisplay"}}
|
|
if err := e.EncodeToken(display); err != nil {
|
|
return err
|
|
}
|
|
|
|
cfg := xml.StartElement{Name: xml.Name{Local: "clockConfig"}}
|
|
|
|
if r.TimeZone != "" {
|
|
cfg.Attr = append(cfg.Attr, xml.Attr{
|
|
Name: xml.Name{Local: "timezoneInfo"},
|
|
Value: r.TimeZone,
|
|
})
|
|
}
|
|
|
|
if r.Enabled != nil {
|
|
cfg.Attr = append(cfg.Attr, xml.Attr{
|
|
Name: xml.Name{Local: "userEnable"},
|
|
Value: strconv.FormatBool(*r.Enabled),
|
|
})
|
|
}
|
|
|
|
if r.Format != "" {
|
|
if wire := mapToWireFormat(r.Format); wire != "" {
|
|
cfg.Attr = append(cfg.Attr, xml.Attr{
|
|
Name: xml.Name{Local: "timeFormat"},
|
|
Value: wire,
|
|
})
|
|
}
|
|
}
|
|
|
|
if r.Brightness != nil {
|
|
cfg.Attr = append(cfg.Attr, xml.Attr{
|
|
Name: xml.Name{Local: "brightnessLevel"},
|
|
Value: strconv.Itoa(*r.Brightness),
|
|
})
|
|
}
|
|
|
|
if err := e.EncodeToken(cfg); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := e.EncodeToken(xml.EndElement{Name: cfg.Name}); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := e.EncodeToken(xml.EndElement{Name: display.Name}); err != nil {
|
|
return err
|
|
}
|
|
|
|
return e.Flush()
|
|
}
|