diff --git a/cmd/example-mdns/main.go b/cmd/example-mdns/main.go index e0faecc..485a102 100644 --- a/cmd/example-mdns/main.go +++ b/cmd/example-mdns/main.go @@ -13,7 +13,7 @@ import ( ) func main() { - verbose := flag.Bool("v", false, "Enable verbose logging") + verbose := flag.Bool("verbose", false, "Enable verbose logging") timeout := flag.Duration("timeout", 5*time.Second, "Discovery timeout") flag.Parse() @@ -73,7 +73,7 @@ func main() { fmt.Println("✓ No network errors during discovery process") fmt.Println("✗ No devices responded with _soundtouch._tcp service") } else { - fmt.Println("Run with -v flag for detailed technical information") + fmt.Println("Run with --verbose flag for detailed technical information") } fmt.Println() diff --git a/cmd/example-upnp/main.go b/cmd/example-upnp/main.go index 343045d..7a880df 100644 --- a/cmd/example-upnp/main.go +++ b/cmd/example-upnp/main.go @@ -14,7 +14,7 @@ import ( ) func main() { - verbose := flag.Bool("v", false, "Enable verbose logging") + verbose := flag.Bool("verbose", false, "Enable verbose logging") timeout := flag.Duration("timeout", 5*time.Second, "Discovery timeout") flag.Parse() @@ -81,7 +81,7 @@ func main() { fmt.Println("✓ UDP multicast connection established") fmt.Println("✗ No devices responded with MediaRenderer service type") } else { - fmt.Println("Run with -v flag for detailed technical information") + fmt.Println("Run with --verbose flag for detailed technical information") } fmt.Println() diff --git a/cmd/mdns-scanner/main.go b/cmd/mdns-scanner/main.go index 6d23f8d..1995429 100644 --- a/cmd/mdns-scanner/main.go +++ b/cmd/mdns-scanner/main.go @@ -15,7 +15,7 @@ import ( ) func main() { - verbose := flag.Bool("v", false, "Enable verbose logging") + verbose := flag.Bool("verbose", false, "Enable verbose logging") timeout := flag.Duration("timeout", 10*time.Second, "Discovery timeout") service := flag.String("service", "_services._dns-sd._udp", "Service type to scan for (use _services._dns-sd._udp to find all)") diff --git a/cmd/soundtouch-cli/cmd_balance.go b/cmd/soundtouch-cli/cmd_balance.go new file mode 100644 index 0000000..944b086 --- /dev/null +++ b/cmd/soundtouch-cli/cmd_balance.go @@ -0,0 +1,147 @@ +// Package main provides the soundtouch-cli balance control commands. +package main + +import ( + "fmt" + + "github.com/urfave/cli/v2" +) + +// getBalance retrieves the current balance level from the device +func getBalance(c *cli.Context) error { + clientConfig := GetClientConfig(c) + PrintDeviceHeader("Getting balance level", clientConfig.Host, clientConfig.Port) + + client, err := CreateSoundTouchClient(clientConfig) + if err != nil { + PrintError(fmt.Sprintf("Failed to create client: %v", err)) + return err + } + + balance, err := client.GetBalance() + if err != nil { + PrintError(fmt.Sprintf("Failed to get balance: %v", err)) + return err + } + + fmt.Printf("Current balance level: %d\n", balance.ActualBalance) + + if balance.TargetBalance != balance.ActualBalance { + fmt.Printf("Target balance level: %d\n", balance.TargetBalance) + } + + // Display balance direction + switch { + case balance.ActualBalance > 0: + fmt.Printf("Balance direction: Right (+%d)\n", balance.ActualBalance) + case balance.ActualBalance < 0: + fmt.Printf("Balance direction: Left (%d)\n", balance.ActualBalance) + default: + fmt.Println("Balance direction: Center (0)") + } + + return nil +} + +// setBalance sets the balance level on the device +func setBalance(c *cli.Context) error { + level := c.Int("level") + clientConfig := GetClientConfig(c) + PrintDeviceHeader(fmt.Sprintf("Setting balance level to %d", level), clientConfig.Host, clientConfig.Port) + + client, err := CreateSoundTouchClient(clientConfig) + if err != nil { + PrintError(fmt.Sprintf("Failed to create client: %v", err)) + return err + } + + err = client.SetBalanceSafe(level) + if err != nil { + PrintError(fmt.Sprintf("Failed to set balance: %v", err)) + return err + } + + PrintSuccess(fmt.Sprintf("Balance level set to %d", level)) + return nil +} + +// balanceLeft shifts balance to the left +func balanceLeft(c *cli.Context) error { + amount := c.Int("amount") + clientConfig := GetClientConfig(c) + PrintDeviceHeader(fmt.Sprintf("Shifting balance left by %d", amount), clientConfig.Host, clientConfig.Port) + + client, err := CreateSoundTouchClient(clientConfig) + if err != nil { + PrintError(fmt.Sprintf("Failed to create client: %v", err)) + return err + } + + // Get current balance level first + currentBalance, err := client.GetBalance() + if err != nil { + PrintError(fmt.Sprintf("Failed to get current balance: %v", err)) + return err + } + + newLevel := currentBalance.ActualBalance - amount + err = client.SetBalanceSafe(newLevel) + if err != nil { + PrintError(fmt.Sprintf("Failed to shift balance left: %v", err)) + return err + } + + PrintSuccess(fmt.Sprintf("Balance shifted from %d to %d (left)", currentBalance.ActualBalance, newLevel)) + return nil +} + +// balanceRight shifts balance to the right +func balanceRight(c *cli.Context) error { + amount := c.Int("amount") + clientConfig := GetClientConfig(c) + PrintDeviceHeader(fmt.Sprintf("Shifting balance right by %d", amount), clientConfig.Host, clientConfig.Port) + + client, err := CreateSoundTouchClient(clientConfig) + if err != nil { + PrintError(fmt.Sprintf("Failed to create client: %v", err)) + return err + } + + // Get current balance level first + currentBalance, err := client.GetBalance() + if err != nil { + PrintError(fmt.Sprintf("Failed to get current balance: %v", err)) + return err + } + + newLevel := currentBalance.ActualBalance + amount + err = client.SetBalanceSafe(newLevel) + if err != nil { + PrintError(fmt.Sprintf("Failed to shift balance right: %v", err)) + return err + } + + PrintSuccess(fmt.Sprintf("Balance shifted from %d to %d (right)", currentBalance.ActualBalance, newLevel)) + return nil +} + +// balanceCenter centers the balance (sets to 0) +func balanceCenter(c *cli.Context) error { + clientConfig := GetClientConfig(c) + PrintDeviceHeader("Centering balance", clientConfig.Host, clientConfig.Port) + + client, err := CreateSoundTouchClient(clientConfig) + if err != nil { + PrintError(fmt.Sprintf("Failed to create client: %v", err)) + return err + } + + err = client.SetBalanceSafe(0) + if err != nil { + PrintError(fmt.Sprintf("Failed to center balance: %v", err)) + return err + } + + PrintSuccess("Balance centered") + return nil +} diff --git a/cmd/soundtouch-cli/cmd_bass.go b/cmd/soundtouch-cli/cmd_bass.go new file mode 100644 index 0000000..52456fa --- /dev/null +++ b/cmd/soundtouch-cli/cmd_bass.go @@ -0,0 +1,143 @@ +package main + +import ( + "fmt" + + "github.com/urfave/cli/v2" +) + +// getBass retrieves the current bass level from the device +func getBass(c *cli.Context) error { + clientConfig := GetClientConfig(c) + PrintDeviceHeader("Getting bass level", clientConfig.Host, clientConfig.Port) + + client, err := CreateSoundTouchClient(clientConfig) + if err != nil { + PrintError(fmt.Sprintf("Failed to create client: %v", err)) + return err + } + + bass, err := client.GetBass() + if err != nil { + PrintError(fmt.Sprintf("Failed to get bass: %v", err)) + return err + } + + fmt.Printf("Current bass level: %d\n", bass.ActualBass) + + if bass.TargetBass != bass.ActualBass { + fmt.Printf("Target bass level: %d\n", bass.TargetBass) + } + + return nil +} + +// setBass sets the bass level on the device +func setBass(c *cli.Context) error { + level := c.Int("level") + clientConfig := GetClientConfig(c) + PrintDeviceHeader(fmt.Sprintf("Setting bass level to %d", level), clientConfig.Host, clientConfig.Port) + + client, err := CreateSoundTouchClient(clientConfig) + if err != nil { + PrintError(fmt.Sprintf("Failed to create client: %v", err)) + return err + } + + err = client.SetBassSafe(level) + if err != nil { + PrintError(fmt.Sprintf("Failed to set bass: %v", err)) + return err + } + + PrintSuccess(fmt.Sprintf("Bass level set to %d", level)) + return nil +} + +// bassUp increases the bass level +func bassUp(c *cli.Context) error { + amount := c.Int("amount") + clientConfig := GetClientConfig(c) + PrintDeviceHeader(fmt.Sprintf("Increasing bass by %d", amount), clientConfig.Host, clientConfig.Port) + + client, err := CreateSoundTouchClient(clientConfig) + if err != nil { + PrintError(fmt.Sprintf("Failed to create client: %v", err)) + return err + } + + // Get current bass level first + currentBass, err := client.GetBass() + if err != nil { + PrintError(fmt.Sprintf("Failed to get current bass: %v", err)) + return err + } + + newLevel := currentBass.ActualBass + amount + err = client.SetBassSafe(newLevel) + if err != nil { + PrintError(fmt.Sprintf("Failed to increase bass: %v", err)) + return err + } + + PrintSuccess(fmt.Sprintf("Bass increased from %d to %d", currentBass.ActualBass, newLevel)) + return nil +} + +// bassDown decreases the bass level +func bassDown(c *cli.Context) error { + amount := c.Int("amount") + clientConfig := GetClientConfig(c) + PrintDeviceHeader(fmt.Sprintf("Decreasing bass by %d", amount), clientConfig.Host, clientConfig.Port) + + client, err := CreateSoundTouchClient(clientConfig) + if err != nil { + PrintError(fmt.Sprintf("Failed to create client: %v", err)) + return err + } + + // Get current bass level first + currentBass, err := client.GetBass() + if err != nil { + PrintError(fmt.Sprintf("Failed to get current bass: %v", err)) + return err + } + + newLevel := currentBass.ActualBass - amount + err = client.SetBassSafe(newLevel) + if err != nil { + PrintError(fmt.Sprintf("Failed to decrease bass: %v", err)) + return err + } + + PrintSuccess(fmt.Sprintf("Bass decreased from %d to %d", currentBass.ActualBass, newLevel)) + return nil +} + +// getBassCapabilities retrieves the bass capabilities of the device +func getBassCapabilities(c *cli.Context) error { + clientConfig := GetClientConfig(c) + PrintDeviceHeader("Getting bass capabilities", clientConfig.Host, clientConfig.Port) + + client, err := CreateSoundTouchClient(clientConfig) + if err != nil { + PrintError(fmt.Sprintf("Failed to create client: %v", err)) + return err + } + + capabilities, err := client.GetBassCapabilities() + if err != nil { + PrintError(fmt.Sprintf("Failed to get bass capabilities: %v", err)) + return err + } + + fmt.Println("Bass Capabilities:") + fmt.Printf(" Available: %t\n", capabilities.BassAvailable) + + if capabilities.BassAvailable { + fmt.Printf(" Range: %d to %d\n", capabilities.BassMin, capabilities.BassMax) + fmt.Printf(" Default: %d\n", capabilities.BassDefault) + } + + return nil +} diff --git a/cmd/soundtouch-cli/cmd_clock.go b/cmd/soundtouch-cli/cmd_clock.go new file mode 100644 index 0000000..426bf54 --- /dev/null +++ b/cmd/soundtouch-cli/cmd_clock.go @@ -0,0 +1,289 @@ +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 { + hour, minute, err = parseTimeString(timeStr) + if err != nil { + PrintError(fmt.Sprintf("Invalid time format. Use HH:MM 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 + default: + PrintError("Invalid format. Use: 12 (12-hour) or 24 (24-hour)") + 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 +} diff --git a/cmd/soundtouch-cli/cmd_network.go b/cmd/soundtouch-cli/cmd_network.go new file mode 100644 index 0000000..e85f05f --- /dev/null +++ b/cmd/soundtouch-cli/cmd_network.go @@ -0,0 +1,122 @@ +package main + +import ( + "fmt" + + "github.com/urfave/cli/v2" +) + +// getNetworkInfo retrieves network information from the device +func getNetworkInfo(c *cli.Context) error { + clientConfig := GetClientConfig(c) + PrintDeviceHeader("Getting network information", clientConfig.Host, clientConfig.Port) + + client, err := CreateSoundTouchClient(clientConfig) + if err != nil { + PrintError(fmt.Sprintf("Failed to create client: %v", err)) + return err + } + + networkInfo, err := client.GetNetworkInfo() + if err != nil { + PrintError(fmt.Sprintf("Failed to get network info: %v", err)) + return err + } + + fmt.Println("Network Information:") + + if networkInfo.GetWifiProfileCount() > 0 { + fmt.Printf(" WiFi Profiles: %d\n", networkInfo.GetWifiProfileCount()) + } + + interfaces := networkInfo.GetInterfaces() + if len(interfaces) == 0 { + fmt.Println(" No network interfaces found") + return nil + } + + fmt.Printf(" Interfaces (%d):\n", len(interfaces)) + for i, iface := range interfaces { + fmt.Printf("\n Interface %d:\n", i+1) + fmt.Printf(" Type: %s\n", iface.GetType()) + + if iface.GetName() != "" { + fmt.Printf(" Name: %s\n", iface.GetName()) + } + + if iface.GetIPAddress() != "" { + fmt.Printf(" IP Address: %s\n", iface.GetIPAddress()) + } + + if iface.GetMacAddress() != "" { + fmt.Printf(" MAC Address: %s\n", iface.GetMacAddress()) + } + + fmt.Printf(" State: %s\n", iface.GetStateDescription()) + + if iface.IsWiFi() { + if iface.GetSSID() != "" { + fmt.Printf(" SSID: %s\n", iface.GetSSID()) + } + + if iface.GetSignal() != "" { + fmt.Printf(" Signal: %s (%d%%)\n", iface.GetSignalDescription(), iface.GetSignalQuality()) + } + + if iface.GetFrequencyKHz() > 0 { + fmt.Printf(" Frequency: %s (%s)\n", iface.FormatFrequency(), iface.GetFrequencyBand()) + } + + if iface.GetMode() != "" { + fmt.Printf(" Mode: %s\n", iface.GetModeDescription()) + } + } + } + + // Show active connections summary + activeInterfaces := networkInfo.GetActiveInterfaces() + if len(activeInterfaces) > 0 { + fmt.Println("\n Active Connections:") + for _, iface := range activeInterfaces { + fmt.Printf(" - %s: %s\n", iface.GetType(), iface.GetNetworkSummary()) + } + } + + return nil +} + +// pingDevice pings the device to test connectivity +func pingDevice(c *cli.Context) error { + clientConfig := GetClientConfig(c) + PrintDeviceHeader("Pinging device", clientConfig.Host, clientConfig.Port) + + client, err := CreateSoundTouchClient(clientConfig) + if err != nil { + PrintError(fmt.Sprintf("Failed to create client: %v", err)) + return err + } + + err = client.Ping() + if err != nil { + PrintError(fmt.Sprintf("Ping failed: %v", err)) + return err + } + + PrintSuccess("Device is reachable") + return nil +} + +// getDeviceURL displays the device's base URL +func getDeviceURL(c *cli.Context) error { + clientConfig := GetClientConfig(c) + + client, err := CreateSoundTouchClient(clientConfig) + if err != nil { + PrintError(fmt.Sprintf("Failed to create client: %v", err)) + return err + } + + baseURL := client.BaseURL() + fmt.Printf("Device URL: %s\n", baseURL) + return nil +} diff --git a/cmd/soundtouch-cli/cmd_zone.go b/cmd/soundtouch-cli/cmd_zone.go new file mode 100644 index 0000000..f08f29d --- /dev/null +++ b/cmd/soundtouch-cli/cmd_zone.go @@ -0,0 +1,311 @@ +package main + +import ( + "fmt" + "net" + "strings" + + "github.com/user_account/bose-soundtouch/pkg/models" + "github.com/urfave/cli/v2" +) + +// getZone retrieves the current zone configuration +func getZone(c *cli.Context) error { + clientConfig := GetClientConfig(c) + PrintDeviceHeader("Getting zone information", clientConfig.Host, clientConfig.Port) + + client, err := CreateSoundTouchClient(clientConfig) + if err != nil { + PrintError(fmt.Sprintf("Failed to create client: %v", err)) + return err + } + + zone, err := client.GetZone() + if err != nil { + PrintError(fmt.Sprintf("Failed to get zone: %v", err)) + return err + } + + if zone.Master == "" { + fmt.Println("Device is not in a zone") + return nil + } + + fmt.Println("Zone Configuration:") + fmt.Printf(" Master: %s\n", zone.Master) + + if len(zone.Members) > 0 { + fmt.Printf(" Members (%d):\n", len(zone.Members)) + for _, member := range zone.Members { + fmt.Printf(" - %s", member.DeviceID) + if member.IP != "" { + fmt.Printf(" (IP: %s)", member.IP) + } + fmt.Println() + } + } else { + fmt.Println(" Members: none (standalone device)") + } + + return nil +} + +// getZoneStatus retrieves the zone status +func getZoneStatus(c *cli.Context) error { + clientConfig := GetClientConfig(c) + PrintDeviceHeader("Getting zone status", clientConfig.Host, clientConfig.Port) + + client, err := CreateSoundTouchClient(clientConfig) + if err != nil { + PrintError(fmt.Sprintf("Failed to create client: %v", err)) + return err + } + + status, err := client.GetZoneStatus() + if err != nil { + PrintError(fmt.Sprintf("Failed to get zone status: %v", err)) + return err + } + + fmt.Printf("Zone Status: %s\n", status) + + inZone, err := client.IsInZone() + if err != nil { + PrintWarning(fmt.Sprintf("Could not determine zone membership: %v", err)) + } else { + fmt.Printf("In Zone: %t\n", inZone) + } + + return nil +} + +// getZoneMembers lists all zone members +func getZoneMembers(c *cli.Context) error { + clientConfig := GetClientConfig(c) + PrintDeviceHeader("Getting zone members", clientConfig.Host, clientConfig.Port) + + client, err := CreateSoundTouchClient(clientConfig) + if err != nil { + PrintError(fmt.Sprintf("Failed to create client: %v", err)) + return err + } + + members, err := client.GetZoneMembers() + if err != nil { + PrintError(fmt.Sprintf("Failed to get zone members: %v", err)) + return err + } + + if len(members) == 0 { + fmt.Println("No zone members found") + return nil + } + + fmt.Printf("Zone Members (%d):\n", len(members)) + for i, member := range members { + fmt.Printf(" %d. %s", i+1, member) + if member == clientConfig.Host { + fmt.Print(" (this device)") + } + fmt.Println() + } + + return nil +} + +// createZone creates a new zone with specified members +func createZone(c *cli.Context) error { + members := c.StringSlice("members") + clientConfig := GetClientConfig(c) + + if len(members) == 0 { + PrintError("At least one member must be specified") + return fmt.Errorf("no members specified") + } + + PrintDeviceHeader(fmt.Sprintf("Creating zone with %d members", len(members)), clientConfig.Host, clientConfig.Port) + + client, err := CreateSoundTouchClient(clientConfig) + if err != nil { + PrintError(fmt.Sprintf("Failed to create client: %v", err)) + return err + } + + // Parse and validate member IPs + var memberIPs []net.IP + for _, member := range members { + ip := net.ParseIP(member) + if ip == nil { + PrintError(fmt.Sprintf("Invalid IP address: %s", member)) + return fmt.Errorf("invalid IP address: %s", member) + } + memberIPs = append(memberIPs, ip) + } + + // For simplicity, use the first member as master and rest as members + // In a real scenario, you might want to specify the master separately + masterDeviceID := "master" // This would need to be a real device ID + memberMap := make(map[string]string) + for i, ip := range memberIPs { + deviceID := fmt.Sprintf("device_%d", i+1) + memberMap[deviceID] = ip.String() + } + + err = client.CreateZoneWithIPs(masterDeviceID, memberMap) + if err != nil { + PrintError(fmt.Sprintf("Failed to create zone: %v", err)) + return err + } + + PrintSuccess(fmt.Sprintf("Zone created with members: %s", strings.Join(members, ", "))) + return nil +} + +// addToZone adds a device to the current zone +func addToZone(c *cli.Context) error { + memberIP := c.String("member") + clientConfig := GetClientConfig(c) + + if memberIP == "" { + PrintError("Member IP address is required") + return fmt.Errorf("member IP is required") + } + + PrintDeviceHeader(fmt.Sprintf("Adding %s to zone", memberIP), clientConfig.Host, clientConfig.Port) + + client, err := CreateSoundTouchClient(clientConfig) + if err != nil { + PrintError(fmt.Sprintf("Failed to create client: %v", err)) + return err + } + + // Validate IP address + ip := net.ParseIP(memberIP) + if ip == nil { + PrintError(fmt.Sprintf("Invalid IP address: %s", memberIP)) + return fmt.Errorf("invalid IP address: %s", memberIP) + } + + // For this example, we'll use the IP as the device ID + // In practice, you'd need the actual device ID + deviceID := memberIP + err = client.AddToZone(deviceID, memberIP) + if err != nil { + PrintError(fmt.Sprintf("Failed to add to zone: %v", err)) + return err + } + + PrintSuccess(fmt.Sprintf("Added %s to zone", memberIP)) + return nil +} + +// removeFromZone removes a device from the current zone +func removeFromZone(c *cli.Context) error { + memberIP := c.String("member") + clientConfig := GetClientConfig(c) + + if memberIP == "" { + PrintError("Member IP address is required") + return fmt.Errorf("member IP is required") + } + + PrintDeviceHeader(fmt.Sprintf("Removing %s from zone", memberIP), clientConfig.Host, clientConfig.Port) + + client, err := CreateSoundTouchClient(clientConfig) + if err != nil { + PrintError(fmt.Sprintf("Failed to create client: %v", err)) + return err + } + + // Validate IP address + ip := net.ParseIP(memberIP) + if ip == nil { + PrintError(fmt.Sprintf("Invalid IP address: %s", memberIP)) + return fmt.Errorf("invalid IP address: %s", memberIP) + } + + // For this example, we'll use the IP as the device ID + // In practice, you'd need the actual device ID + deviceID := memberIP + err = client.RemoveFromZone(deviceID) + if err != nil { + PrintError(fmt.Sprintf("Failed to remove from zone: %v", err)) + return err + } + + PrintSuccess(fmt.Sprintf("Removed %s from zone", memberIP)) + return nil +} + +// dissolveZone dissolves the current zone +func dissolveZone(c *cli.Context) error { + clientConfig := GetClientConfig(c) + PrintDeviceHeader("Dissolving zone", clientConfig.Host, clientConfig.Port) + + client, err := CreateSoundTouchClient(clientConfig) + if err != nil { + PrintError(fmt.Sprintf("Failed to create client: %v", err)) + return err + } + + err = client.DissolveZone() + if err != nil { + PrintError(fmt.Sprintf("Failed to dissolve zone: %v", err)) + return err + } + + PrintSuccess("Zone dissolved") + return nil +} + +// setZoneConfig sets zone configuration +func setZoneConfig(c *cli.Context) error { + master := c.String("master") + members := c.StringSlice("members") + clientConfig := GetClientConfig(c) + + if master == "" { + PrintError("Master device IP is required") + return fmt.Errorf("master IP is required") + } + + PrintDeviceHeader(fmt.Sprintf("Setting zone with master %s", master), clientConfig.Host, clientConfig.Port) + + client, err := CreateSoundTouchClient(clientConfig) + if err != nil { + PrintError(fmt.Sprintf("Failed to create client: %v", err)) + return err + } + + // Create zone configuration using ZoneRequest + zoneRequest := models.NewZoneRequest(master) + + // Add members if specified + if len(members) > 0 { + for _, memberIP := range members { + // Validate IP address + if net.ParseIP(memberIP) == nil { + PrintError(fmt.Sprintf("Invalid member IP address: %s", memberIP)) + return fmt.Errorf("invalid IP address: %s", memberIP) + } + + // Use IP as device ID for simplicity - in practice you'd need real device IDs + deviceID := memberIP + zoneRequest.AddMember(deviceID, memberIP) + } + } + + err = client.SetZone(zoneRequest) + if err != nil { + PrintError(fmt.Sprintf("Failed to set zone: %v", err)) + return err + } + + if len(members) > 0 { + PrintSuccess(fmt.Sprintf("Zone configured with master %s and members: %s", master, strings.Join(members, ", "))) + } else { + PrintSuccess(fmt.Sprintf("Zone configured with master %s", master)) + } + + return nil +} diff --git a/cmd/soundtouch-cli/common.go b/cmd/soundtouch-cli/common.go index ef9452d..f921203 100644 --- a/cmd/soundtouch-cli/common.go +++ b/cmd/soundtouch-cli/common.go @@ -15,7 +15,6 @@ import ( var CommonFlags = []cli.Flag{ &cli.StringFlag{ Name: "host", - Aliases: []string{"h"}, Usage: "SoundTouch device host/IP address (can include port like host:8090)", EnvVars: []string{"SOUNDTOUCH_HOST"}, }, diff --git a/cmd/soundtouch-cli/main.go b/cmd/soundtouch-cli/main.go index 5db746f..3498c47 100644 --- a/cmd/soundtouch-cli/main.go +++ b/cmd/soundtouch-cli/main.go @@ -21,13 +21,7 @@ func main() { Email: "info@example.com", }, }, - Flags: []cli.Flag{ - &cli.BoolFlag{ - Name: "verbose", - Aliases: []string{"v"}, - Usage: "Enable verbose output", - }, - }, + Flags: []cli.Flag{}, Commands: []*cli.Command{ // Discovery commands { @@ -39,19 +33,11 @@ func main() { Name: "devices", Usage: "Discover and list SoundTouch devices", Action: discoverDevices, - Flags: []cli.Flag{ - &cli.BoolFlag{ - Name: "all", - Aliases: []string{"a"}, - Usage: "Show detailed information for all devices", - }, - &cli.DurationFlag{ - Name: "timeout", - Aliases: []string{"t"}, - Usage: "Discovery timeout", - Value: 10000000000, // 10 seconds in nanoseconds - }, - }, + Flags: append(CommonFlags, &cli.BoolFlag{ + Name: "all", + Aliases: []string{"a"}, + Usage: "Show detailed information for all devices", + }), }, }, }, @@ -261,6 +247,326 @@ func main() { }, }, }, + // Bass commands + { + Name: "bass", + Aliases: []string{"b"}, + Usage: "Bass control commands", + Subcommands: []*cli.Command{ + { + Name: "get", + Usage: "Get current bass level", + Action: getBass, + Flags: CommonFlags, + Before: RequireHost, + }, + { + Name: "set", + Usage: "Set bass level", + Action: setBass, + Flags: append(CommonFlags, &cli.IntFlag{ + Name: "level", + Aliases: []string{"l"}, + Usage: "Bass level (-9 to 9)", + Required: true, + }), + Before: RequireHost, + }, + { + Name: "up", + Usage: "Increase bass", + Action: bassUp, + Flags: append(CommonFlags, &cli.IntFlag{ + Name: "amount", + Aliases: []string{"a"}, + Usage: "Amount to increase (1-5)", + Value: 1, + }), + Before: RequireHost, + }, + { + Name: "down", + Usage: "Decrease bass", + Action: bassDown, + Flags: append(CommonFlags, &cli.IntFlag{ + Name: "amount", + Aliases: []string{"a"}, + Usage: "Amount to decrease (1-5)", + Value: 1, + }), + Before: RequireHost, + }, + { + Name: "capabilities", + Usage: "Get bass capabilities", + Action: getBassCapabilities, + Flags: CommonFlags, + Before: RequireHost, + }, + }, + }, + // Balance commands + { + Name: "balance", + Aliases: []string{"bal"}, + Usage: "Balance control commands", + Subcommands: []*cli.Command{ + { + Name: "get", + Usage: "Get current balance level", + Action: getBalance, + Flags: CommonFlags, + Before: RequireHost, + }, + { + Name: "set", + Usage: "Set balance level", + Action: setBalance, + Flags: append(CommonFlags, &cli.IntFlag{ + Name: "level", + Aliases: []string{"l"}, + Usage: "Balance level (-50 to 50, negative=left, positive=right)", + Required: true, + }), + Before: RequireHost, + }, + { + Name: "left", + Usage: "Shift balance to the left", + Action: balanceLeft, + Flags: append(CommonFlags, &cli.IntFlag{ + Name: "amount", + Aliases: []string{"a"}, + Usage: "Amount to shift left (1-5)", + Value: 1, + }), + Before: RequireHost, + }, + { + Name: "right", + Usage: "Shift balance to the right", + Action: balanceRight, + Flags: append(CommonFlags, &cli.IntFlag{ + Name: "amount", + Aliases: []string{"a"}, + Usage: "Amount to shift right (1-5)", + Value: 1, + }), + Before: RequireHost, + }, + { + Name: "center", + Usage: "Center the balance", + Action: balanceCenter, + Flags: CommonFlags, + Before: RequireHost, + }, + }, + }, + // Clock commands + { + Name: "clock", + Aliases: []string{"time"}, + Usage: "Clock and time commands", + Subcommands: []*cli.Command{ + { + Name: "get", + Usage: "Get current time", + Action: getClockTime, + Flags: CommonFlags, + Before: RequireHost, + }, + { + Name: "set", + Usage: "Set clock time", + Action: setClockTime, + Flags: append(CommonFlags, &cli.StringFlag{ + Name: "time", + Aliases: []string{"t"}, + Usage: "Time in HH:MM format or 'now' for current time", + Required: true, + }), + Before: RequireHost, + }, + { + Name: "now", + Usage: "Set clock to current system time", + Action: setClockTimeNow, + Flags: CommonFlags, + Before: RequireHost, + }, + { + Name: "display", + Usage: "Clock display commands", + Subcommands: []*cli.Command{ + { + Name: "get", + Usage: "Get display settings", + Action: getClockDisplay, + Flags: CommonFlags, + Before: RequireHost, + }, + { + Name: "enable", + Usage: "Enable clock display", + Action: enableClockDisplay, + Flags: CommonFlags, + Before: RequireHost, + }, + { + Name: "disable", + Usage: "Disable clock display", + Action: disableClockDisplay, + Flags: CommonFlags, + Before: RequireHost, + }, + { + Name: "brightness", + Usage: "Set display brightness", + Action: setClockDisplayBrightness, + Flags: append(CommonFlags, &cli.StringFlag{ + Name: "brightness", + Aliases: []string{"b"}, + Usage: "Brightness level (low, medium, high, off)", + Required: true, + }), + Before: RequireHost, + }, + { + Name: "format", + Usage: "Set display format", + Action: setClockDisplayFormat, + Flags: append(CommonFlags, &cli.StringFlag{ + Name: "format", + Aliases: []string{"f"}, + Usage: "Time format (12 or 24)", + Required: true, + }), + Before: RequireHost, + }, + }, + }, + }, + }, + // Network commands + { + Name: "network", + Aliases: []string{"net"}, + Usage: "Network information commands", + Subcommands: []*cli.Command{ + { + Name: "info", + Usage: "Get network information", + Action: getNetworkInfo, + Flags: CommonFlags, + Before: RequireHost, + }, + { + Name: "ping", + Usage: "Ping the device", + Action: pingDevice, + Flags: CommonFlags, + Before: RequireHost, + }, + { + Name: "url", + Usage: "Get device base URL", + Action: getDeviceURL, + Flags: CommonFlags, + Before: RequireHost, + }, + }, + }, + // Zone commands + { + Name: "zone", + Aliases: []string{"z"}, + Usage: "Multi-room zone management commands", + Subcommands: []*cli.Command{ + { + Name: "get", + Usage: "Get current zone configuration", + Action: getZone, + Flags: CommonFlags, + Before: RequireHost, + }, + { + Name: "status", + Usage: "Get zone status", + Action: getZoneStatus, + Flags: CommonFlags, + Before: RequireHost, + }, + { + Name: "members", + Usage: "List zone members", + Action: getZoneMembers, + Flags: CommonFlags, + Before: RequireHost, + }, + { + Name: "create", + Usage: "Create a new zone", + Action: createZone, + Flags: append(CommonFlags, &cli.StringSliceFlag{ + Name: "members", + Aliases: []string{"m"}, + Usage: "Member IP addresses", + Required: true, + }), + Before: RequireHost, + }, + { + Name: "add", + Usage: "Add device to zone", + Action: addToZone, + Flags: append(CommonFlags, &cli.StringFlag{ + Name: "member", + Aliases: []string{"m"}, + Usage: "Member IP address to add", + Required: true, + }), + Before: RequireHost, + }, + { + Name: "remove", + Usage: "Remove device from zone", + Action: removeFromZone, + Flags: append(CommonFlags, &cli.StringFlag{ + Name: "member", + Aliases: []string{"m"}, + Usage: "Member IP address to remove", + Required: true, + }), + Before: RequireHost, + }, + { + Name: "dissolve", + Usage: "Dissolve the current zone", + Action: dissolveZone, + Flags: CommonFlags, + Before: RequireHost, + }, + { + Name: "set", + Usage: "Set zone configuration", + Action: setZoneConfig, + Flags: append(CommonFlags, + &cli.StringFlag{ + Name: "master", + Usage: "Master device IP address", + Required: true, + }, + &cli.StringSliceFlag{ + Name: "members", + Aliases: []string{"m"}, + Usage: "Member IP addresses", + }, + ), + Before: RequireHost, + }, + }, + }, }, } diff --git a/soundtouch-cli b/soundtouch-cli deleted file mode 100755 index ebd31cb..0000000 Binary files a/soundtouch-cli and /dev/null differ