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 are rejected with // "Error parsing request"): // // // // // // 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 // 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 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 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 // 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() }