diff --git a/examples/away/main.go b/examples/away/main.go new file mode 100644 index 0000000..147158a --- /dev/null +++ b/examples/away/main.go @@ -0,0 +1,112 @@ +package main + +import ( + "context" + "fmt" + "os" + "time" + + "github.com/gonzolino/gotado" +) + +const ( + clientID = "tado-web-app" + clientSecret = "wZaRN7rpjn3FoNyF5IFuxg9uMzYJcvOoQ8QWiIqS3hfk6gLhVlG57j5YNoZL2Rtc" +) + +func main() { + // Get credentials from env vars + username, ok := os.LookupEnv("TADO_USERNAME") + if !ok { + fmt.Fprintf(os.Stderr, "Variable TADO_USERNAME not set\n") + os.Exit(1) + } + password, ok := os.LookupEnv("TADO_PASSWORD") + if !ok { + fmt.Fprintf(os.Stderr, "Variable TADO_PASSWORD not set\n") + os.Exit(1) + } + + if len(os.Args) != 3 { + fmt.Fprintf(os.Stderr, "Usage: %s homeName zoneName\n", os.Args[0]) + os.Exit(1) + } + homeName, zoneName := os.Args[1], os.Args[2] + + ctx := context.Background() + + // Create authenticated tado° client + client := gotado.NewClient(clientID, clientSecret).WithTimeout(5 * time.Second) + client, err := client.WithCredentials(ctx, username, password) + if err != nil { + fmt.Fprintf(os.Stderr, "Authentication failed: %v\n", err) + os.Exit(1) + } + + user, err := gotado.GetMe(client) + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to get user info: %v\n", err) + os.Exit(1) + } + + // Find the home to control + var home *gotado.UserHome + for _, h := range user.Homes { + if h.Name == homeName { + home = &h + break + } + } + if home == nil { + fmt.Fprintf(os.Stderr, "Home '%s' not found\n", homeName) + os.Exit(1) + } + + // Find zone to control + zones, err := gotado.GetZones(client, home) + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to get zones: %v\n", err) + os.Exit(1) + } + var zone *gotado.Zone + for _, z := range zones { + if z.Name == zoneName { + zone = z + break + } + } + if zone == nil { + fmt.Fprintf(os.Stderr, "Zone '%s' not found\n", zoneName) + os.Exit(1) + } + + // Show away configuration + awayConfig, err := gotado.GetAwayConfiguration(client, home, zone) + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to get away configuration: %v\n", err) + os.Exit(1) + } + fmt.Println("Away Configuration:") + if awayConfig.AutoAdjust { + fmt.Printf("Comfort Level: %d\n", awayConfig.ComfortLevel) + } else { + fmt.Printf("Temperature: %.2f C°, %.2f F°\n", awayConfig.Setting.Temperature.Celsius, awayConfig.Setting.Temperature.Fahrenheit) + } + + // Update comfort level + err = gotado.SetAwayComfortLevel(client, home, zone, 0) + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to get set comfort level: %v\n", err) + os.Exit(1) + } + + fmt.Printf("Set comfort level for away mode in home '%s', zone '%s' to 'Eco'\n", home.Name, zone.Name) + time.Sleep(10 * time.Second) + + // Restore original away configuration + if err := gotado.SetAwayConfiguration(client, home, zone, awayConfig); err != nil { + fmt.Fprintf(os.Stderr, "Failed to set away configuration: %v\n", err) + os.Exit(1) + } + fmt.Printf("Restored original away configuration in home '%s', zone '%s'\n", home.Name, zone.Name) +} diff --git a/tado.go b/tado.go index b4b3f60..175561e 100644 --- a/tado.go +++ b/tado.go @@ -252,6 +252,28 @@ type ScheduleBlockSettingTemperature struct { Fahrenheit float64 `json:"fahrenheit"` } +// AwayConfiguration holds the settings to use when everybody leaves the house +type AwayConfiguration struct { + Type string `json:"type"` + AutoAdjust bool `json:"autoAdjust"` + // Comfort Level must be 0 (Eco), 50 (Balanced) or 100 (Comfort) + ComfortLevel int32 `json:"comfortLevel"` + Setting *AwayConfigurationSetting `json:"setting"` +} + +// AwayConfigurationSetting holds the setting of an away configuration +type AwayConfigurationSetting struct { + Type string `json:"type"` + Power string `json:"power"` + Temperature *AwayConfigurationSettingTemperature `json:"temperature,omitempty"` +} + +// AwayConfigurationSettingTemperature holds the temperature of an away configuration setting +type AwayConfigurationSettingTemperature struct { + Celsius float64 `json:"celsius"` + Fahrenheit float64 `json:"fahrenheit"` +} + // PresenceLock holds a locked presence setting for a home type PresenceLock struct { HomePresence string `json:"homePresence"` @@ -608,6 +630,88 @@ func SetSchedule(client *Client, userHome *UserHome, zone *Zone, timetable *Sche return nil } +// GetAwayConfiguration returns the away configuration of the given zone +func GetAwayConfiguration(client *Client, userHome *UserHome, zone *Zone) (*AwayConfiguration, error) { + resp, err := client.Request(http.MethodGet, apiURL("homes/%d/zones/%d/schedule/awayConfiguration", userHome.ID, zone.ID), nil) + if err != nil { + return nil, err + } + + if err := isError(resp); err != nil { + return nil, fmt.Errorf("tado° API error: %w", err) + } + + awayConfig := &AwayConfiguration{} + if err := json.NewDecoder(resp.Body).Decode(&awayConfig); err != nil { + return nil, fmt.Errorf("unable to decode tado° API response: %w", err) + } + + return awayConfig, nil +} + +// SetAwayConfiguration sets an away configuration for the given zone +func SetAwayConfiguration(client *Client, userHome *UserHome, zone *Zone, awayConfig *AwayConfiguration) error { + data, err := json.Marshal(awayConfig) + if err != nil { + return fmt.Errorf("unable to marshal away configuration: %w", err) + } + req, err := http.NewRequest(http.MethodPut, apiURL("homes/%d/zones/%d/schedule/awayConfiguration", userHome.ID, zone.ID), bytes.NewReader(data)) + if err != nil { + return fmt.Errorf("unable to create http request: %w", err) + } + req.Header.Set("Content-Type", "application/json;charset=utf-8") + resp, err := client.Do(req) + if err != nil { + return err + } + + if err := isError(resp); err != nil { + return fmt.Errorf("tado° API error: %w", err) + } + + return nil +} + +// SetAwayTemperature sets the manual temperature for a zone when everybody leaves the house +func SetAwayTemperature(client *Client, userHome *UserHome, zone *Zone, temperature float64) error { + home, err := GetHome(client, userHome) + if err != nil || home == nil { + return fmt.Errorf("unable to determine temperature unit") + } + temperatureSetting := &AwayConfigurationSettingTemperature{} + switch home.TemperatureUnit { + case "CELSIUS": + temperatureSetting.Celsius = temperature + case "FAHRENHEIT": + temperatureSetting.Fahrenheit = temperature + default: + return fmt.Errorf("invalid temperature unit '%s'", home.TemperatureUnit) + } + + awayConfig := &AwayConfiguration{ + Type: "HEATING", + AutoAdjust: false, + Setting: &AwayConfigurationSetting{ + Type: "HEATING", + Power: "ON", + Temperature: temperatureSetting, + }, + } + + return SetAwayConfiguration(client, userHome, zone, awayConfig) +} + +// SetAwayComfortLevel sets the away configuration to auto-adjust at the given comfort level ("preheat"). +// Allowed values got the comfort level are 0, 50 and 100 (Eco, Balanced, Comfort) +func SetAwayComfortLevel(client *Client, userHome *UserHome, zone *Zone, comfortLevel int32) error { + awayConfig := &AwayConfiguration{ + Type: "HEATING", + AutoAdjust: true, + ComfortLevel: comfortLevel, + } + return SetAwayConfiguration(client, userHome, zone, awayConfig) +} + // setPresenceLock sets a locked presence on the given home (HOME or AWAY) func setPresenceLock(client *Client, userHome *UserHome, presence PresenceLock) error { data, err := json.Marshal(presence)