Files
Bose-SoundTouch/cmd/soundtouch-cli/cmd_clock.go
T
Tobias Gesellchen 433368788b feat: complete CLI feature parity with original main.go
🎉 100% Feature Complete CLI Refactoring

MAJOR IMPROVEMENTS:
- Refactored from monolithic 149-complexity main.go to modular urfave/cli structure
- Added ALL missing commands for complete feature parity (47/47 features)
- Reduced golangci-lint issues by 70% (108+ → 32)

NEW COMMAND FILES:
- cmd_bass.go - Bass control (get/set/up/down/capabilities)
- cmd_balance.go - Balance control (get/set/left/right/center)
- cmd_clock.go - Clock management (time + display settings)
- cmd_network.go - Network information (info/ping/URL)
- cmd_zone.go - Multi-room zones (get/create/add/remove/dissolve)
- cmd_playback.go - Enhanced with key commands (volume-up/down, power, mute, etc.)
- cmd_info.go - Enhanced with preset selection and track info

COMPLETE FEATURE MAPPING:
 Discovery: discover devices [--all]
 Device Info: info, name get/set, capabilities, presets, track
 Playback: play start/pause/stop/next/prev, key send/power/mute/thumbs-up/down/volume-up/down
 Volume: volume get/set/up/down (with safety warnings, defaults: ±2)
 Bass: bass get/set/up/down/capabilities (defaults: ±1)
 Balance: balance get/set/left/right/center (defaults: ±5)
 Sources: source list/select/spotify/bluetooth/aux (with account support)
 Clock: clock get/set/now + display enable/disable/brightness/format (supports Unix timestamps, auto format)
 Network: network info/ping/url
 Zones: zone get/status/members/create/add/remove/dissolve/set
 Presets: preset selection by number (1-6)

PRESERVED FEATURES:
 All safety warnings and limits maintained
 Default increment/decrement values preserved
 Environment variable support (SOUNDTOUCH_HOST, SOUNDTOUCH_PORT)
 Extended format support (Unix timestamps, 'now', 'auto' clock format)
 Source account parameters for streaming services
 Zone deviceID@ip format support

ENHANCED USER EXPERIENCE:
 Organized subcommand hierarchy instead of 47 flat flags
 Comprehensive help system for each command
 Consistent flag naming (--host, --port, --timeout)
 Rich error messages with success/warning indicators
 Input validation and safety checks

ARCHITECTURAL IMPROVEMENTS:
 Modular command structure for better maintainability
 Shared utilities in common.go
 Consistent error handling and client configuration
 Clean separation of concerns

EXAMPLES:
# Discovery
soundtouch-cli discover devices --all --timeout 15s

# Volume control
soundtouch-cli volume set --host 192.168.1.100 --level 50
soundtouch-cli volume up --host 192.168.1.100 --amount 3

# Bass/Balance control
soundtouch-cli bass set --host 192.168.1.100 --level 3
soundtouch-cli balance left --host 192.168.1.100 --amount 5

# Key commands
soundtouch-cli key power --host 192.168.1.100
soundtouch-cli key send --host 192.168.1.100 --key SHUFFLE_ON

# Source selection
soundtouch-cli source select --host 192.168.1.100 --source SPOTIFY --account myaccount
soundtouch-cli source bluetooth --host 192.168.1.100

# Clock management
soundtouch-cli clock set --host 192.168.1.100 --time now
soundtouch-cli clock display format --host 192.168.1.100 --format 24

# Zone management
soundtouch-cli zone create --host 192.168.1.100 --members 192.168.1.101,192.168.1.102

Breaking Changes:
- CLI now uses subcommands instead of flat flags (functional equivalent provided for all commands)

RESULT: Transformed a 149-complexity monolithic CLI into a clean, organized,
feature-complete tool with 100% functionality preservation and significant UX improvements! 🚀
2026-01-09 23:54:10 +01:00

302 lines
8.7 KiB
Go

// Package main provides the soundtouch-cli clock control commands.
package main
import (
"fmt"
"strconv"
"time"
"github.com/user_account/bose-soundtouch/pkg/models"
"github.com/urfave/cli/v2"
)
// getClockTime retrieves the current clock time from the device
func getClockTime(c *cli.Context) error {
clientConfig := GetClientConfig(c)
PrintDeviceHeader("Getting clock time", clientConfig.Host, clientConfig.Port)
client, err := CreateSoundTouchClient(clientConfig)
if err != nil {
PrintError(fmt.Sprintf("Failed to create client: %v", err))
return err
}
clockTime, err := client.GetClockTime()
if err != nil {
PrintError(fmt.Sprintf("Failed to get clock time: %v", err))
return err
}
if timeObj, err := clockTime.GetTime(); err == nil {
fmt.Printf("Current time: %02d:%02d\n", timeObj.Hour(), timeObj.Minute())
fmt.Printf("UTC time: %s\n", timeObj.Format("2006-01-02 15:04:05 MST"))
} else {
fmt.Printf("Time value: %s\n", clockTime.Value)
}
if clockTime.GetUTC() > 0 {
utcTime := time.Unix(clockTime.GetUTC(), 0)
fmt.Printf("UTC timestamp: %d (%s)\n", clockTime.GetUTC(), utcTime.Format("2006-01-02 15:04:05 MST"))
}
if clockTime.GetZone() != "" {
fmt.Printf("Time zone: %s\n", clockTime.GetZone())
}
return nil
}
// setClockTime sets the clock time on the device
func setClockTime(c *cli.Context) error {
timeStr := c.String("time")
clientConfig := GetClientConfig(c)
// Parse time string (HH:MM format)
var hour, minute int
var err error
if timeStr == "now" {
now := time.Now()
hour = now.Hour()
minute = now.Minute()
PrintDeviceHeader(fmt.Sprintf("Setting clock time to current time (%02d:%02d)", hour, minute), clientConfig.Host, clientConfig.Port)
} else {
// Try to parse as Unix timestamp first
if timestamp, parseErr := strconv.ParseInt(timeStr, 10, 64); parseErr == nil {
targetTime := time.Unix(timestamp, 0)
hour = targetTime.Hour()
minute = targetTime.Minute()
PrintDeviceHeader(fmt.Sprintf("Setting clock time from Unix timestamp %d (%02d:%02d)", timestamp, hour, minute), clientConfig.Host, clientConfig.Port)
} else {
// Parse as HH:MM format
hour, minute, err = parseTimeString(timeStr)
if err != nil {
PrintError(fmt.Sprintf("Invalid time format. Use HH:MM, Unix timestamp, or 'now': %v", err))
return err
}
PrintDeviceHeader(fmt.Sprintf("Setting clock time to %02d:%02d", hour, minute), clientConfig.Host, clientConfig.Port)
}
}
client, err := CreateSoundTouchClient(clientConfig)
if err != nil {
PrintError(fmt.Sprintf("Failed to create client: %v", err))
return err
}
// Create a time object for today with the specified hour and minute
now := time.Now()
targetTime := time.Date(now.Year(), now.Month(), now.Day(), hour, minute, 0, 0, now.Location())
clockTimeRequest := models.NewClockTimeRequest(targetTime)
err = client.SetClockTime(clockTimeRequest)
if err != nil {
PrintError(fmt.Sprintf("Failed to set clock time: %v", err))
return err
}
PrintSuccess(fmt.Sprintf("Clock time set to %02d:%02d", hour, minute))
return nil
}
// setClockTimeNow sets the clock time to the current system time
func setClockTimeNow(c *cli.Context) error {
clientConfig := GetClientConfig(c)
PrintDeviceHeader("Setting clock time to current system time", clientConfig.Host, clientConfig.Port)
client, err := CreateSoundTouchClient(clientConfig)
if err != nil {
PrintError(fmt.Sprintf("Failed to create client: %v", err))
return err
}
err = client.SetClockTimeNow()
if err != nil {
PrintError(fmt.Sprintf("Failed to set clock time: %v", err))
return err
}
now := time.Now()
PrintSuccess(fmt.Sprintf("Clock time set to current time (%02d:%02d)", now.Hour(), now.Minute()))
return nil
}
// getClockDisplay retrieves the current clock display settings
func getClockDisplay(c *cli.Context) error {
clientConfig := GetClientConfig(c)
PrintDeviceHeader("Getting clock display settings", clientConfig.Host, clientConfig.Port)
client, err := CreateSoundTouchClient(clientConfig)
if err != nil {
PrintError(fmt.Sprintf("Failed to create client: %v", err))
return err
}
clockDisplay, err := client.GetClockDisplay()
if err != nil {
PrintError(fmt.Sprintf("Failed to get clock display: %v", err))
return err
}
fmt.Println("Clock Display Settings:")
fmt.Printf(" Enabled: %t\n", clockDisplay.IsEnabled())
fmt.Printf(" Brightness: %d (%s)\n", clockDisplay.GetBrightness(), clockDisplay.GetBrightnessLevel())
fmt.Printf(" Format: %s (%s)\n", clockDisplay.GetFormat(), clockDisplay.GetFormatDescription())
if clockDisplay.IsAutoDimEnabled() {
fmt.Printf(" Auto-dim: enabled\n")
}
if clockDisplay.GetTimeZone() != "" {
fmt.Printf(" Time zone: %s\n", clockDisplay.GetTimeZone())
}
return nil
}
// enableClockDisplay enables the clock display
func enableClockDisplay(c *cli.Context) error {
clientConfig := GetClientConfig(c)
PrintDeviceHeader("Enabling clock display", clientConfig.Host, clientConfig.Port)
client, err := CreateSoundTouchClient(clientConfig)
if err != nil {
PrintError(fmt.Sprintf("Failed to create client: %v", err))
return err
}
err = client.EnableClockDisplay()
if err != nil {
PrintError(fmt.Sprintf("Failed to enable clock display: %v", err))
return err
}
PrintSuccess("Clock display enabled")
return nil
}
// disableClockDisplay disables the clock display
func disableClockDisplay(c *cli.Context) error {
clientConfig := GetClientConfig(c)
PrintDeviceHeader("Disabling clock display", clientConfig.Host, clientConfig.Port)
client, err := CreateSoundTouchClient(clientConfig)
if err != nil {
PrintError(fmt.Sprintf("Failed to create client: %v", err))
return err
}
err = client.DisableClockDisplay()
if err != nil {
PrintError(fmt.Sprintf("Failed to disable clock display: %v", err))
return err
}
PrintSuccess("Clock display disabled")
return nil
}
// setClockDisplayBrightness sets the clock display brightness
func setClockDisplayBrightness(c *cli.Context) error {
brightness := c.String("brightness")
clientConfig := GetClientConfig(c)
PrintDeviceHeader(fmt.Sprintf("Setting clock display brightness to %s", brightness), clientConfig.Host, clientConfig.Port)
client, err := CreateSoundTouchClient(clientConfig)
if err != nil {
PrintError(fmt.Sprintf("Failed to create client: %v", err))
return err
}
// Convert brightness string to numeric value
var brightnessLevel int
switch brightness {
case "low", "LOW":
brightnessLevel = 25
case "medium", "MEDIUM", "med":
brightnessLevel = 50
case "high", "HIGH":
brightnessLevel = 100
case "off", "OFF":
brightnessLevel = 0
default:
PrintError("Invalid brightness. Use: low, medium, high, or off")
return fmt.Errorf("invalid brightness value")
}
err = client.SetClockDisplayBrightness(brightnessLevel)
if err != nil {
PrintError(fmt.Sprintf("Failed to set clock display brightness: %v", err))
return err
}
PrintSuccess(fmt.Sprintf("Clock display brightness set to %s", brightness))
return nil
}
// setClockDisplayFormat sets the clock display format
func setClockDisplayFormat(c *cli.Context) error {
format := c.String("format")
clientConfig := GetClientConfig(c)
PrintDeviceHeader(fmt.Sprintf("Setting clock display format to %s", format), clientConfig.Host, clientConfig.Port)
client, err := CreateSoundTouchClient(clientConfig)
if err != nil {
PrintError(fmt.Sprintf("Failed to create client: %v", err))
return err
}
// Validate and normalize format value
var formatSetting models.ClockFormat
switch format {
case "12", "12h", "12hour":
formatSetting = models.ClockFormat12Hour
case "24", "24h", "24hour":
formatSetting = models.ClockFormat24Hour
case "auto":
formatSetting = models.ClockFormatAuto
default:
PrintError("Invalid format. Use: 12 (12-hour), 24 (24-hour), or auto")
return fmt.Errorf("invalid format value")
}
err = client.SetClockDisplayFormat(formatSetting)
if err != nil {
PrintError(fmt.Sprintf("Failed to set clock display format: %v", err))
return err
}
PrintSuccess(fmt.Sprintf("Clock display format set to %s", format))
return nil
}
// parseTimeString parses a time string in HH:MM format
func parseTimeString(timeStr string) (int, int, error) {
if len(timeStr) != 5 || timeStr[2] != ':' {
return 0, 0, fmt.Errorf("time must be in HH:MM format")
}
hourStr := timeStr[0:2]
minuteStr := timeStr[3:5]
hour, err := strconv.Atoi(hourStr)
if err != nil {
return 0, 0, fmt.Errorf("invalid hour: %s", hourStr)
}
minute, err := strconv.Atoi(minuteStr)
if err != nil {
return 0, 0, fmt.Errorf("invalid minute: %s", minuteStr)
}
if hour < 0 || hour > 23 {
return 0, 0, fmt.Errorf("hour must be between 0 and 23")
}
if minute < 0 || minute > 59 {
return 0, 0, fmt.Errorf("minute must be between 0 and 59")
}
return hour, minute, nil
}