├── UnitTest ├── MSTestSettings.cs ├── UnitTest.csproj ├── BaseTest.cs ├── AuthenticationTest.cs ├── DataRetrievalTest.cs └── DataSetTest.cs ├── KoenZomers.Tado.Api.snk ├── Api ├── Models │ ├── HomeState.cs │ ├── ConnectionState.cs │ ├── EarlyStart.cs │ ├── Characteristics.cs │ ├── Link.cs │ ├── ActivityDataPoints.cs │ ├── MobileDevice │ │ ├── Settings.cs │ │ ├── BearingFromHome.cs │ │ ├── Details.cs │ │ ├── Location.cs │ │ └── Item.cs │ ├── Home.cs │ ├── Geolocation.cs │ ├── Precision.cs │ ├── Temperature.cs │ ├── ZoneSummary.cs │ ├── MountingState.cs │ ├── Overlay.cs │ ├── SensorDataPoints.cs │ ├── Capability.cs │ ├── DazzleMode.cs │ ├── Temperatures.cs │ ├── ContactDetails.cs │ ├── TemperatureSteps.cs │ ├── Humidity.cs │ ├── WeatherState.cs │ ├── HeatingPower.cs │ ├── SolarIntensity.cs │ ├── Weather.cs │ ├── OpenWindowDetection.cs │ ├── Setting.cs │ ├── Installation.cs │ ├── InsideTemperature.cs │ ├── OutsideTemperature.cs │ ├── Address.cs │ ├── Termination.cs │ ├── User.cs │ ├── Authentication │ │ ├── DeviceAuthorizationResponse.cs │ │ └── Token.cs │ ├── House.cs │ ├── Session.cs │ ├── Device.cs │ ├── Zone.cs │ └── State.cs ├── Enums │ ├── PowerStates.cs │ ├── DeviceTypes.cs │ ├── HomePresence.cs │ ├── DurationModes.cs │ └── TerminationTypes.cs ├── Controllers │ ├── Base.cs │ └── Http.cs ├── Exceptions │ ├── SessionAuthenticationFailedException.cs │ ├── AuthenticationExpiredException.cs │ ├── RequestFailedException.cs │ ├── SessionNotAuthenticatedException.cs │ └── RequestThrottledException.cs ├── Helpers │ ├── EnumValidation.cs │ └── QueryStringBuilder.cs ├── Converters │ ├── PowerStatesConverter.cs │ ├── DeviceTypeConverter.cs │ └── DurationModeConverter.cs ├── Configuration │ └── Tado.cs ├── .NET API.csproj ├── Extensions │ └── TadoService.cs └── KoenZomers.Tado.Api.xml ├── appsettings.test.json ├── .github └── workflows │ ├── cibuild.yml │ └── pushnugetpackage.yml ├── appsettings.json ├── .gitattributes ├── KoenZomers.Tado.sln ├── .gitignore ├── README.md └── LICENSE /UnitTest/MSTestSettings.cs: -------------------------------------------------------------------------------- 1 | [assembly: Parallelize(Scope = ExecutionScope.MethodLevel)] 2 | -------------------------------------------------------------------------------- /KoenZomers.Tado.Api.snk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KoenZomers/TadoApi/HEAD/KoenZomers.Tado.Api.snk -------------------------------------------------------------------------------- /Api/Models/HomeState.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace KoenZomers.Tado.Api.Models; 4 | 5 | /// 6 | /// Information about the state of the home 7 | /// 8 | public class HomeState 9 | { 10 | [JsonPropertyName("presence")] 11 | public string Presence { get; set; } 12 | } -------------------------------------------------------------------------------- /Api/Enums/PowerStates.cs: -------------------------------------------------------------------------------- 1 | namespace KoenZomers.Tado.Api.Enums; 2 | 3 | /// 4 | /// Defines the power state a Tado device can be in 5 | /// 6 | public enum PowerStates : short 7 | { 8 | /// 9 | /// Device is ON 10 | /// 11 | On, 12 | 13 | /// 14 | /// Device is OFF 15 | /// 16 | Off 17 | } -------------------------------------------------------------------------------- /Api/Enums/DeviceTypes.cs: -------------------------------------------------------------------------------- 1 | namespace KoenZomers.Tado.Api.Enums; 2 | 3 | /// 4 | /// Defines the types of Tado devices that can be switched 5 | /// 6 | public enum DeviceTypes : short 7 | { 8 | /// 9 | /// Heating 10 | /// 11 | Heating, 12 | 13 | /// 14 | /// Hot water 15 | /// 16 | HotWater 17 | } -------------------------------------------------------------------------------- /Api/Enums/HomePresence.cs: -------------------------------------------------------------------------------- 1 | namespace KoenZomers.Tado.Api.Enums; 2 | 3 | /// 4 | /// Defines the possible states home presence can be in 5 | /// 6 | public enum HomePresence 7 | { 8 | /// 9 | /// At least someone is home 10 | /// 11 | Home, 12 | 13 | /// 14 | /// Everyone is away 15 | /// 16 | Away, 17 | } -------------------------------------------------------------------------------- /Api/Models/ConnectionState.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace KoenZomers.Tado.Api.Models; 4 | 5 | /// 6 | /// State of the connection towards a Tado device 7 | /// 8 | public partial class ConnectionState 9 | { 10 | [JsonPropertyName("value")] 11 | public bool Value { get; set; } 12 | 13 | [JsonPropertyName("timestamp")] 14 | public DateTime Timestamp { get; set; } 15 | } -------------------------------------------------------------------------------- /Api/Models/EarlyStart.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace KoenZomers.Tado.Api.Models; 4 | 5 | /// 6 | /// Indicates whether the Early Start feature is enabled 7 | /// 8 | public class EarlyStart 9 | { 10 | /// 11 | /// Whether Early Start is enabled for the schedule 12 | /// 13 | [JsonPropertyName("enabled")] 14 | public bool? Enabled { get; set; } 15 | } 16 | -------------------------------------------------------------------------------- /Api/Models/Characteristics.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace KoenZomers.Tado.Api.Entities; 4 | 5 | /// 6 | /// Characteristics of a device 7 | /// 8 | public class Characteristics 9 | { 10 | /// 11 | /// The list of capabilities supported by the device 12 | /// 13 | [JsonPropertyName("capabilities")] 14 | public string[]? Capabilities { get; set; } 15 | } 16 | -------------------------------------------------------------------------------- /Api/Models/Link.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace KoenZomers.Tado.Api.Entities; 4 | 5 | /// 6 | /// Represents the link state of a Tado device or component 7 | /// 8 | public partial class Link 9 | { 10 | /// 11 | /// The current state of the link (e.g., ONLINE, OFFLINE) 12 | /// 13 | [JsonPropertyName("state")] 14 | public string? State { get; set; } 15 | } 16 | -------------------------------------------------------------------------------- /Api/Models/ActivityDataPoints.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace KoenZomers.Tado.Api.Entities; 4 | 5 | /// 6 | /// Contains activity data points such as heating power 7 | /// 8 | public class ActivityDataPoints 9 | { 10 | /// 11 | /// The heating power data point 12 | /// 13 | [JsonPropertyName("heatingPower")] 14 | public HeatingPower? HeatingPower { get; set; } 15 | } 16 | -------------------------------------------------------------------------------- /Api/Models/MobileDevice/Settings.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace KoenZomers.Tado.Api.Models.MobileDevice; 4 | 5 | /// 6 | /// Contains settings specific to the device 7 | /// 8 | public class Settings 9 | { 10 | /// 11 | /// Indicates whether geolocation tracking is enabled for the device 12 | /// 13 | [JsonPropertyName("geoTrackingEnabled")] 14 | public bool? GeoTrackingEnabled { get; set; } 15 | } 16 | -------------------------------------------------------------------------------- /Api/Controllers/Base.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | 3 | namespace KoenZomers.Tado.Api.Controllers; 4 | 5 | /// 6 | /// Base class for all controllers 7 | /// 8 | /// Instance through which to allow logging 9 | public abstract class Base(ILoggerFactory loggerFactory) 10 | { 11 | /// 12 | /// Instance through which to perform logging 13 | /// 14 | protected ILogger Logger => loggerFactory.CreateLogger(GetType()); 15 | } 16 | -------------------------------------------------------------------------------- /Api/Models/Home.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace KoenZomers.Tado.Api.Models; 4 | 5 | /// 6 | /// Contains information about a home where Tado is being used 7 | /// 8 | public class Home 9 | { 10 | /// 11 | /// The unique identifier of the home 12 | /// 13 | [JsonPropertyName("id")] 14 | public long? Id { get; set; } 15 | 16 | /// 17 | /// The name of the home 18 | /// 19 | [JsonPropertyName("name")] 20 | public string? Name { get; set; } 21 | } 22 | -------------------------------------------------------------------------------- /Api/Models/Geolocation.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace KoenZomers.Tado.Api.Entities; 4 | 5 | /// 6 | /// Represents the geographic location of a device or home 7 | /// 8 | public class Geolocation 9 | { 10 | /// 11 | /// The latitude coordinate 12 | /// 13 | [JsonPropertyName("latitude")] 14 | public double? Latitude { get; set; } 15 | 16 | /// 17 | /// The longitude coordinate 18 | /// 19 | [JsonPropertyName("longitude")] 20 | public double? Longitude { get; set; } 21 | } 22 | -------------------------------------------------------------------------------- /Api/Models/Precision.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace KoenZomers.Tado.Api.Entities; 4 | 5 | /// 6 | /// Represents the precision of a temperature reading 7 | /// 8 | public partial class Precision 9 | { 10 | /// 11 | /// The precision in Celsius 12 | /// 13 | [JsonPropertyName("celsius")] 14 | public double? Celsius { get; set; } 15 | 16 | /// 17 | /// The precision in Fahrenheit 18 | /// 19 | [JsonPropertyName("fahrenheit")] 20 | public double? Fahrenheit { get; set; } 21 | } 22 | -------------------------------------------------------------------------------- /Api/Models/Temperature.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace KoenZomers.Tado.Api.Models; 4 | 5 | /// 6 | /// Information regarding a temperature 7 | /// 8 | public partial class Temperature 9 | { 10 | /// 11 | /// The temperature in degrees Celsius 12 | /// 13 | [JsonPropertyName("celsius")] 14 | public double? Celsius { get; set; } 15 | 16 | /// 17 | /// The temperature in degrees Fahrenheit 18 | /// 19 | [JsonPropertyName("fahrenheit")] 20 | public double? Fahrenheit { get; set; } 21 | } 22 | -------------------------------------------------------------------------------- /Api/Enums/DurationModes.cs: -------------------------------------------------------------------------------- 1 | namespace KoenZomers.Tado.Api.Enums; 2 | 3 | /// 4 | /// Defines the modes of the duration of changing a temperature 5 | /// 6 | public enum DurationModes : short 7 | { 8 | /// 9 | /// Keep the setting until the next scheduled event starts 10 | /// 11 | UntilNextTimedEvent, 12 | 13 | /// 14 | /// Keep the setting for a specific duration 15 | /// 16 | Timer, 17 | 18 | /// 19 | /// Keep the setting until the user makes another change 20 | /// 21 | UntilNextManualChange 22 | } -------------------------------------------------------------------------------- /Api/Exceptions/SessionAuthenticationFailedException.cs: -------------------------------------------------------------------------------- 1 | 2 | namespace KoenZomers.Tado.Api.Exceptions; 3 | 4 | /// 5 | /// Exception thrown when authenticating failed 6 | /// 7 | public class SessionAuthenticationFailedException : Exception 8 | { 9 | private const string defaultMessage = "Failed to authenticate session"; 10 | 11 | public SessionAuthenticationFailedException() : base(defaultMessage) 12 | { 13 | } 14 | 15 | public SessionAuthenticationFailedException(Exception innerException, string message = defaultMessage) : base(message, innerException) 16 | { 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Api/Exceptions/AuthenticationExpiredException.cs: -------------------------------------------------------------------------------- 1 | namespace KoenZomers.Tado.Api.Exceptions; 2 | 3 | /// 4 | /// Exception thrown when the authentication session expired 5 | /// 6 | public class AuthenticationExpiredException : Exception 7 | { 8 | private const string defaultMessage = "The authentication has expired. You have to reauthenticate."; 9 | 10 | public AuthenticationExpiredException() : base(defaultMessage) 11 | { 12 | } 13 | 14 | public AuthenticationExpiredException(Exception innerException, string message = defaultMessage) : base(message, innerException) 15 | { 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Api/Models/ZoneSummary.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace KoenZomers.Tado.Api.Models; 4 | 5 | /// 6 | /// Summarized state of a zone 7 | /// 8 | public class ZoneSummary 9 | { 10 | /// 11 | /// The current state of the Tado device 12 | /// 13 | [JsonPropertyName("setting")] 14 | public Setting? Setting { get; set; } 15 | 16 | /// 17 | /// Information on when the current state of the Tado device will end 18 | /// 19 | [JsonPropertyName("termination")] 20 | public Termination? Termination { get; set; } 21 | } 22 | -------------------------------------------------------------------------------- /Api/Exceptions/RequestFailedException.cs: -------------------------------------------------------------------------------- 1 | namespace KoenZomers.Tado.Api.Exceptions; 2 | 3 | /// 4 | /// Exception thrown when a request failed 5 | /// 6 | public class RequestFailedException : Exception 7 | { 8 | /// 9 | /// Uri that was called 10 | /// 11 | public Uri Uri { get; private set; } 12 | 13 | public RequestFailedException(Uri uri) : base("A request failed") 14 | { 15 | Uri = uri; 16 | } 17 | 18 | public RequestFailedException(Uri uri, Exception innerException) : base("A request failed", innerException) 19 | { 20 | Uri = uri; 21 | } 22 | } -------------------------------------------------------------------------------- /Api/Models/MountingState.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace KoenZomers.Tado.Api.Entities; 4 | 5 | /// 6 | /// State of the mounted Tado device 7 | /// 8 | public class MountingState 9 | { 10 | /// 11 | /// The current mounting state of the device (e.g., MOUNTED, UNMOUNTED) 12 | /// 13 | [JsonPropertyName("value")] 14 | public string? Value { get; set; } 15 | 16 | /// 17 | /// The timestamp when the mounting state was recorded 18 | /// 19 | [JsonPropertyName("timestamp")] 20 | public DateTime? Timestamp { get; set; } 21 | } 22 | -------------------------------------------------------------------------------- /Api/Models/Overlay.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace KoenZomers.Tado.Api.Models; 4 | 5 | /// 6 | /// Represents the current overlay state of a Tado device 7 | /// 8 | public partial class Overlay 9 | { 10 | /// 11 | /// The current setting applied to the Tado device 12 | /// 13 | [JsonPropertyName("setting")] 14 | public Setting? Setting { get; set; } 15 | 16 | /// 17 | /// Information on when the current setting will end 18 | /// 19 | [JsonPropertyName("termination")] 20 | public Termination? Termination { get; set; } 21 | } 22 | -------------------------------------------------------------------------------- /Api/Models/SensorDataPoints.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace KoenZomers.Tado.Api.Models; 4 | 5 | /// 6 | /// Temperature and humidity measured by a Tado device 7 | /// 8 | public partial class SensorDataPoints 9 | { 10 | /// 11 | /// The inside temperature data point 12 | /// 13 | [JsonPropertyName("insideTemperature")] 14 | public InsideTemperature? InsideTemperature { get; set; } 15 | 16 | /// 17 | /// The humidity data point 18 | /// 19 | [JsonPropertyName("humidity")] 20 | public Humidity? Humidity { get; set; } 21 | } 22 | -------------------------------------------------------------------------------- /appsettings.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "Tado": { 3 | 4 | }, 5 | "Logging": { 6 | "LogLevel": { 7 | // Fatal: Critical errors that cause the application to crash or stop entirely. 8 | // Error: Serious issues that prevent part of the application from functioning correctly. 9 | // Warning: Unexpected events that don't stop the application but may lead to future issues. 10 | // Information: General events that describe the normal flow of the application. 11 | // Debug: Detailed information useful during development and debugging. 12 | // Trace: The most detailed level of logging, capturing every step of execution. 13 | 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /Api/Models/Capability.cs: -------------------------------------------------------------------------------- 1 | using KoenZomers.Tado.Api.Entities; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace KoenZomers.Tado.Api.Models; 5 | 6 | /// 7 | /// Represents a capability of a Tado device, such as temperature control 8 | /// 9 | public class Capability 10 | { 11 | /// 12 | /// The type of capability (e.g., HEATING, COOLING) 13 | /// 14 | [JsonPropertyName("type")] 15 | public string? PurpleType { get; set; } 16 | 17 | /// 18 | /// The temperature-related capabilities 19 | /// 20 | [JsonPropertyName("temperatures")] 21 | public Temperatures? Temperatures { get; set; } 22 | } 23 | -------------------------------------------------------------------------------- /Api/Models/MobileDevice/BearingFromHome.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace KoenZomers.Tado.Api.Models.MobileDevice; 4 | 5 | /// 6 | /// Contains the coordinates relative to the home location where Tado is being used 7 | /// 8 | public partial class BearingFromHome 9 | { 10 | /// 11 | /// The direction in degrees from the home location 12 | /// 13 | [JsonPropertyName("degrees")] 14 | public double? Degrees { get; set; } 15 | 16 | /// 17 | /// The direction in radians from the home location 18 | /// 19 | [JsonPropertyName("radians")] 20 | public double? Radians { get; set; } 21 | } 22 | -------------------------------------------------------------------------------- /Api/Models/DazzleMode.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace KoenZomers.Tado.Api.Models; 4 | 5 | /// 6 | /// Details about the current configuration of the dazzle mode (animation when changing the temperature) 7 | /// 8 | public partial class DazzleMode 9 | { 10 | /// 11 | /// Indicates whether the dazzle mode feature is supported 12 | /// 13 | [JsonPropertyName("supported")] 14 | public bool? Supported { get; set; } 15 | 16 | /// 17 | /// Indicates whether the dazzle mode feature is currently enabled 18 | /// 19 | [JsonPropertyName("enabled")] 20 | public bool? Enabled { get; set; } 21 | } 22 | -------------------------------------------------------------------------------- /Api/Models/Temperatures.cs: -------------------------------------------------------------------------------- 1 | using KoenZomers.Tado.Api.Models; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace KoenZomers.Tado.Api.Entities; 5 | 6 | /// 7 | /// Represents temperature step settings in both Celsius and Fahrenheit 8 | /// 9 | public partial class Temperatures 10 | { 11 | /// 12 | /// Temperature step settings in Celsius 13 | /// 14 | [JsonPropertyName("celsius")] 15 | public TemperatureSteps? Celsius { get; set; } 16 | 17 | /// 18 | /// Temperature step settings in Fahrenheit 19 | /// 20 | [JsonPropertyName("fahrenheit")] 21 | public TemperatureSteps? Fahrenheit { get; set; } 22 | } 23 | -------------------------------------------------------------------------------- /Api/Exceptions/SessionNotAuthenticatedException.cs: -------------------------------------------------------------------------------- 1 | namespace KoenZomers.Tado.Api.Exceptions; 2 | 3 | /// 4 | /// Exception thrown when functionality is called which requires the session to be authenticated while it isn't yet 5 | /// 6 | public class SessionNotAuthenticatedException : Exception 7 | { 8 | private const string defaultMessage = "This session has not yet been authenticated.Please call Authenticate() first."; 9 | 10 | public SessionNotAuthenticatedException() : base(defaultMessage) 11 | { 12 | } 13 | 14 | public SessionNotAuthenticatedException(Exception innerException, string message = defaultMessage) : base(message, innerException) 15 | { 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Api/Enums/TerminationTypes.cs: -------------------------------------------------------------------------------- 1 | namespace KoenZomers.Tado.Api.Enums; 2 | 3 | /// 4 | /// Defines the types to which a Tado device can be set that define the end of the current state of the device 5 | /// 6 | public enum TerminationTypes2 : short 7 | { 8 | /// 9 | /// The state will not change unless the device gets explicit instructions to do so 10 | /// 11 | Manual, 12 | 13 | /// 14 | /// The state of the device will change at the next scheduled event for the device 15 | /// 16 | NextScheduledEvent, 17 | 18 | /// 19 | /// The state of the device will change after a preset amount of time has elapsed 20 | /// 21 | Timer 22 | } -------------------------------------------------------------------------------- /Api/Models/ContactDetails.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace KoenZomers.Tado.Api.Models; 4 | 5 | /// 6 | /// Contains contact details of an owner of a house 7 | /// 8 | public class ContactDetails 9 | { 10 | /// 11 | /// The full name of the contact person 12 | /// 13 | [JsonPropertyName("name")] 14 | public string? Name { get; set; } 15 | 16 | /// 17 | /// The email address of the contact person 18 | /// 19 | [JsonPropertyName("email")] 20 | public string? Email { get; set; } 21 | 22 | /// 23 | /// The phone number of the contact person 24 | /// 25 | [JsonPropertyName("phone")] 26 | public string? Phone { get; set; } 27 | } 28 | -------------------------------------------------------------------------------- /Api/Models/TemperatureSteps.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace KoenZomers.Tado.Api.Models; 4 | 5 | /// 6 | /// Represents the minimum, maximum, and step values for temperature settings 7 | /// 8 | public class TemperatureSteps 9 | { 10 | /// 11 | /// The minimum temperature value 12 | /// 13 | [JsonPropertyName("min")] 14 | public long? Min { get; set; } 15 | 16 | /// 17 | /// The maximum temperature value 18 | /// 19 | [JsonPropertyName("max")] 20 | public long? Max { get; set; } 21 | 22 | /// 23 | /// The step increment for temperature adjustments 24 | /// 25 | [JsonPropertyName("step")] 26 | public long? Step { get; set; } 27 | } 28 | -------------------------------------------------------------------------------- /Api/Models/Humidity.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace KoenZomers.Tado.Api.Models; 4 | 5 | /// 6 | /// Humidity measured by a Tado device 7 | /// 8 | public partial class Humidity 9 | { 10 | /// 11 | /// The type of humidity measurement (e.g., PERCENTAGE) 12 | /// 13 | [JsonPropertyName("type")] 14 | public string? CurrentType { get; set; } 15 | 16 | /// 17 | /// The humidity percentage 18 | /// 19 | [JsonPropertyName("percentage")] 20 | public double? Percentage { get; set; } 21 | 22 | /// 23 | /// The timestamp when the humidity was measured 24 | /// 25 | [JsonPropertyName("timestamp")] 26 | public DateTime? Timestamp { get; set; } 27 | } 28 | -------------------------------------------------------------------------------- /Api/Models/WeatherState.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace KoenZomers.Tado.Api.Entities; 4 | 5 | /// 6 | /// Represents the current weather state 7 | /// 8 | public partial class WeatherState 9 | { 10 | /// 11 | /// The type of weather state (e.g., SUNNY, CLOUDY) 12 | /// 13 | [JsonPropertyName("type")] 14 | public string? CurrentType { get; set; } 15 | 16 | /// 17 | /// The value describing the weather condition 18 | /// 19 | [JsonPropertyName("value")] 20 | public string? Value { get; set; } 21 | 22 | /// 23 | /// The timestamp when the weather state was recorded 24 | /// 25 | [JsonPropertyName("timestamp")] 26 | public DateTime? Timestamp { get; set; } 27 | } 28 | -------------------------------------------------------------------------------- /Api/Helpers/EnumValidation.cs: -------------------------------------------------------------------------------- 1 | namespace KoenZomers.Tado.Api.Helpers; 2 | 3 | /// 4 | /// Helpers to work with enums 5 | /// 6 | internal static class EnumValidation 7 | { 8 | /// 9 | /// Ensures the provided enum is valid for the range of allowed enum options 10 | /// 11 | internal static void EnsureEnumWithinRange(TEnum value) where TEnum : struct, IConvertible 12 | { 13 | if (!typeof(TEnum).IsEnum) 14 | { 15 | throw new ArgumentException($"{nameof(TEnum)} must be an enumeration type."); 16 | } 17 | 18 | if (!Enum.IsDefined(typeof(TEnum), value)) 19 | { 20 | throw new ArgumentOutOfRangeException($"Value: '{value}' is not within enumeration range of type: '{typeof(TEnum).Name}'"); 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /Api/Models/HeatingPower.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace KoenZomers.Tado.Api.Entities; 4 | 5 | /// 6 | /// Represents the heating power data point of a device 7 | /// 8 | public class HeatingPower 9 | { 10 | /// 11 | /// The type of heating power (e.g., percentage) 12 | /// 13 | [JsonPropertyName("type")] 14 | public string? CurrentType { get; set; } 15 | 16 | /// 17 | /// The percentage of heating power being used 18 | /// 19 | [JsonPropertyName("percentage")] 20 | public double? Percentage { get; set; } 21 | 22 | /// 23 | /// The timestamp when the heating power was recorded 24 | /// 25 | [JsonPropertyName("timestamp")] 26 | public DateTime? Timestamp { get; set; } 27 | } 28 | -------------------------------------------------------------------------------- /Api/Models/SolarIntensity.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace KoenZomers.Tado.Api.Models; 4 | 5 | /// 6 | /// Represents the solar intensity measured by a Tado device 7 | /// 8 | public partial class SolarIntensity 9 | { 10 | /// 11 | /// The type of solar intensity measurement 12 | /// 13 | [JsonPropertyName("type")] 14 | public string? CurrentType { get; set; } 15 | 16 | /// 17 | /// The percentage of solar intensity 18 | /// 19 | [JsonPropertyName("percentage")] 20 | public double? Percentage { get; set; } 21 | 22 | /// 23 | /// The timestamp when the solar intensity was recorded 24 | /// 25 | [JsonPropertyName("timestamp")] 26 | public DateTime? Timestamp { get; set; } 27 | } 28 | -------------------------------------------------------------------------------- /Api/Models/Weather.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | using KoenZomers.Tado.Api.Entities; 3 | 4 | namespace KoenZomers.Tado.Api.Models; 5 | 6 | /// 7 | /// The current weather 8 | /// 9 | public partial class Weather 10 | { 11 | /// 12 | /// The current solar intensity 13 | /// 14 | [JsonPropertyName("solarIntensity")] 15 | public SolarIntensity? SolarIntensity { get; set; } 16 | 17 | /// 18 | /// The current outside temperature 19 | /// 20 | [JsonPropertyName("outsideTemperature")] 21 | public OutsideTemperature? OutsideTemperature { get; set; } 22 | 23 | /// 24 | /// The current weather state (e.g., SUNNY, CLOUDY) 25 | /// 26 | [JsonPropertyName("weatherState")] 27 | public WeatherState? WeatherState { get; set; } 28 | } 29 | -------------------------------------------------------------------------------- /Api/Models/OpenWindowDetection.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace KoenZomers.Tado.Api.Models; 4 | 5 | /// 6 | /// Open Window Detection settings 7 | /// 8 | public partial class OpenWindowDetection 9 | { 10 | /// 11 | /// Indicates whether the open window detection feature is supported 12 | /// 13 | [JsonPropertyName("supported")] 14 | public bool? Supported { get; set; } 15 | 16 | /// 17 | /// Indicates whether the open window detection feature is enabled 18 | /// 19 | [JsonPropertyName("enabled")] 20 | public bool? Enabled { get; set; } 21 | 22 | /// 23 | /// The timeout duration in seconds for open window detection 24 | /// 25 | [JsonPropertyName("timeoutInSeconds")] 26 | public long? TimeoutInSeconds { get; set; } 27 | } 28 | -------------------------------------------------------------------------------- /Api/Models/MobileDevice/Details.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace KoenZomers.Tado.Api.Models.MobileDevice; 4 | 5 | /// 6 | /// Contains detailed information about a device connected to Tado 7 | /// 8 | public class Details 9 | { 10 | /// 11 | /// The platform of the mobile device (e.g., iOS, Android) 12 | /// 13 | [JsonPropertyName("platform")] 14 | public string? Platform { get; set; } 15 | 16 | /// 17 | /// The operating system version of the mobile device 18 | /// 19 | [JsonPropertyName("osVersion")] 20 | public string? OsVersion { get; set; } 21 | 22 | /// 23 | /// The model of the mobile device 24 | /// 25 | [JsonPropertyName("model")] 26 | public string? Model { get; set; } 27 | 28 | /// 29 | /// The locale setting of the mobile device 30 | /// 31 | [JsonPropertyName("locale")] 32 | public string? Locale { get; set; } 33 | } 34 | -------------------------------------------------------------------------------- /Api/Models/Setting.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace KoenZomers.Tado.Api.Models; 4 | 5 | /// 6 | /// The current state of a Tado device 7 | /// 8 | public partial class Setting 9 | { 10 | /// 11 | /// Type of Tado device 12 | /// 13 | [JsonPropertyName("type")] 14 | [JsonConverter(typeof(Converters.DeviceTypeConverter))] // Ensure this is a System.Text.Json converter 15 | public Enums.DeviceTypes? DeviceType { get; set; } 16 | 17 | /// 18 | /// The power state of the Tado device 19 | /// 20 | [JsonPropertyName("power")] 21 | [JsonConverter(typeof(Converters.PowerStatesConverter))] // Ensure this is a System.Text.Json converter 22 | public Enums.PowerStates? Power { get; set; } 23 | 24 | /// 25 | /// The temperature the Tado device is set to change the zone to 26 | /// 27 | [JsonPropertyName("temperature")] 28 | public Temperature? Temperature { get; set; } 29 | } 30 | -------------------------------------------------------------------------------- /.github/workflows/cibuild.yml: -------------------------------------------------------------------------------- 1 | name: Continuous Integration Build 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | paths-ignore: 7 | - '.github/workflows/**' 8 | pull_request: 9 | branches: [ master ] 10 | paths-ignore: 11 | - '.github/workflows/**' 12 | 13 | jobs: 14 | build: 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - name: Checkout code 19 | uses: actions/checkout@v4 20 | 21 | - name: Setup .NET 9 SDK 22 | uses: actions/setup-dotnet@v4 23 | with: 24 | dotnet-version: '9.0.x' 25 | 26 | - name: Setup .NET 10 SDK 27 | uses: actions/setup-dotnet@v4 28 | with: 29 | dotnet-version: '10.0.x' 30 | 31 | - name: Restore dependencies 32 | run: dotnet restore 33 | 34 | - name: Build Api 35 | run: dotnet build "Api/.NET API.csproj" --no-restore --configuration Release 36 | 37 | - name: Build UnitTest 38 | run: dotnet build "UnitTest/UnitTest.csproj" --no-restore --configuration Release 39 | -------------------------------------------------------------------------------- /Api/Models/Installation.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace KoenZomers.Tado.Api.Models; 4 | 5 | /// 6 | /// One Tado installation 7 | /// 8 | public class Installation 9 | { 10 | /// 11 | /// The unique identifier of the installation 12 | /// 13 | [JsonPropertyName("id")] 14 | public long? Id { get; set; } 15 | 16 | /// 17 | /// The type of the installation 18 | /// 19 | [JsonPropertyName("type")] 20 | public string? CurrentType { get; set; } 21 | 22 | /// 23 | /// The revision number of the installation 24 | /// 25 | [JsonPropertyName("revision")] 26 | public long? Revision { get; set; } 27 | 28 | /// 29 | /// The current state of the installation 30 | /// 31 | [JsonPropertyName("state")] 32 | public string? State { get; set; } 33 | 34 | /// 35 | /// The list of devices included in the installation 36 | /// 37 | public Device[] Devices { get; set; } 38 | } -------------------------------------------------------------------------------- /Api/Models/MobileDevice/Location.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace KoenZomers.Tado.Api.Models.MobileDevice; 4 | 5 | /// 6 | /// Contains the location of a device 7 | /// 8 | public class Location 9 | { 10 | /// 11 | /// Indicates whether the location data is outdated 12 | /// 13 | [JsonPropertyName("stale")] 14 | public bool? Stale { get; set; } 15 | 16 | /// 17 | /// Indicates whether the device is currently at home 18 | /// 19 | [JsonPropertyName("atHome")] 20 | public bool? AtHome { get; set; } 21 | 22 | /// 23 | /// The direction from the home location to the device 24 | /// 25 | [JsonPropertyName("bearingFromHome")] 26 | public BearingFromHome? BearingFromHome { get; set; } 27 | 28 | /// 29 | /// The relative distance of the device from the home fence 30 | /// 31 | [JsonPropertyName("relativeDistanceFromHomeFence")] 32 | public long? RelativeDistanceFromHomeFence { get; set; } 33 | } 34 | -------------------------------------------------------------------------------- /.github/workflows/pushnugetpackage.yml: -------------------------------------------------------------------------------- 1 | name: Manual NuGet Publish 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | build: 8 | name: Building, Packaging and Publishing 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - name: Checkout code 13 | uses: actions/checkout@v4 14 | 15 | - name: Setup .NET 9 SDK 16 | uses: actions/setup-dotnet@v4 17 | with: 18 | dotnet-version: '9.0.x' 19 | 20 | - name: Setup .NET 10 SDK 21 | uses: actions/setup-dotnet@v4 22 | with: 23 | dotnet-version: '10.0.x' 24 | 25 | - name: Restore dependencies 26 | run: dotnet restore 27 | 28 | - name: Build Api 29 | run: dotnet build "Api/.NET API.csproj" --no-restore --configuration Release 30 | 31 | - name: Pack NuGet package 32 | run: dotnet pack "Api/.NET API.csproj" --no-build --configuration Release --output ./nupkg 33 | 34 | - name: Push NuGet package to nuget.org 35 | run: dotnet nuget push ./nupkg/*.nupkg --source https://api.nuget.org/v3/index.json --api-key ${{ secrets.NUGET_API_KEY }} 36 | env: 37 | NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }} 38 | -------------------------------------------------------------------------------- /Api/Models/MobileDevice/Item.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace KoenZomers.Tado.Api.Models.MobileDevice; 4 | 5 | /// 6 | /// Contains information about a mobile device set up to be used with Tado 7 | /// 8 | public class Item 9 | { 10 | /// 11 | /// The name of the mobile device 12 | /// 13 | [JsonPropertyName("name")] 14 | public string? Name { get; set; } 15 | 16 | /// 17 | /// The unique identifier of the mobile device 18 | /// 19 | [JsonPropertyName("id")] 20 | public long? Id { get; set; } 21 | 22 | /// 23 | /// The settings configured for the mobile device 24 | /// 25 | [JsonPropertyName("settings")] 26 | public Settings? Settings { get; set; } 27 | 28 | /// 29 | /// The location information of the mobile device 30 | /// 31 | [JsonPropertyName("location")] 32 | public Location? Location { get; set; } 33 | 34 | /// 35 | /// Metadata details about the mobile device 36 | /// 37 | [JsonPropertyName("deviceMetadata")] 38 | public Details? MobileDeviceDetails { get; set; } 39 | } 40 | -------------------------------------------------------------------------------- /Api/Models/InsideTemperature.cs: -------------------------------------------------------------------------------- 1 | using KoenZomers.Tado.Api.Entities; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace KoenZomers.Tado.Api.Models; 5 | 6 | /// 7 | /// Represents the inside temperature measured by a Tado device 8 | /// 9 | public partial class InsideTemperature 10 | { 11 | /// 12 | /// The temperature in Celsius 13 | /// 14 | [JsonPropertyName("celsius")] 15 | public double? Celsius { get; set; } 16 | 17 | /// 18 | /// The temperature in Fahrenheit 19 | /// 20 | [JsonPropertyName("fahrenheit")] 21 | public double? Fahrenheit { get; set; } 22 | 23 | /// 24 | /// The timestamp when the temperature was recorded 25 | /// 26 | [JsonPropertyName("timestamp")] 27 | public DateTime? Timestamp { get; set; } 28 | 29 | /// 30 | /// The type of temperature reading 31 | /// 32 | [JsonPropertyName("type")] 33 | public string? CurrentType { get; set; } 34 | 35 | /// 36 | /// The precision of the temperature reading 37 | /// 38 | [JsonPropertyName("precision")] 39 | public Precision? Precision { get; set; } 40 | } 41 | -------------------------------------------------------------------------------- /Api/Models/OutsideTemperature.cs: -------------------------------------------------------------------------------- 1 | using KoenZomers.Tado.Api.Entities; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace KoenZomers.Tado.Api.Models; 5 | 6 | /// 7 | /// Represents the outside temperature measured by a Tado device 8 | /// 9 | public partial class OutsideTemperature 10 | { 11 | /// 12 | /// The temperature in Celsius 13 | /// 14 | [JsonPropertyName("celsius")] 15 | public double? Celsius { get; set; } 16 | 17 | /// 18 | /// The temperature in Fahrenheit 19 | /// 20 | [JsonPropertyName("fahrenheit")] 21 | public double? Fahrenheit { get; set; } 22 | 23 | /// 24 | /// The timestamp when the temperature was recorded 25 | /// 26 | [JsonPropertyName("timestamp")] 27 | public DateTime? Timestamp { get; set; } 28 | 29 | /// 30 | /// The type of temperature reading 31 | /// 32 | [JsonPropertyName("type")] 33 | public string? PurpleType { get; set; } 34 | 35 | /// 36 | /// The precision of the temperature reading 37 | /// 38 | [JsonPropertyName("precision")] 39 | public Precision? Precision { get; set; } 40 | } 41 | -------------------------------------------------------------------------------- /Api/Models/Address.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace KoenZomers.Tado.Api.Models; 4 | 5 | /// 6 | /// Represents the address details of a location 7 | /// 8 | public partial class Address 9 | { 10 | /// 11 | /// The first line of the address 12 | /// 13 | [JsonPropertyName("addressLine1")] 14 | public string? AddressLine1 { get; set; } 15 | 16 | /// 17 | /// The second line of the address (optional) 18 | /// 19 | [JsonPropertyName("addressLine2")] 20 | public object? AddressLine2 { get; set; } 21 | 22 | /// 23 | /// The postal or ZIP code 24 | /// 25 | [JsonPropertyName("zipCode")] 26 | public string? ZipCode { get; set; } 27 | 28 | /// 29 | /// The city of the address 30 | /// 31 | [JsonPropertyName("city")] 32 | public string? City { get; set; } 33 | 34 | /// 35 | /// The state or province (optional) 36 | /// 37 | [JsonPropertyName("state")] 38 | public object? State { get; set; } 39 | 40 | /// 41 | /// The country of the address 42 | /// 43 | [JsonPropertyName("country")] 44 | public string? Country { get; set; } 45 | } 46 | -------------------------------------------------------------------------------- /UnitTest/UnitTest.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net9.0;net10.0 5 | latest 6 | enable 7 | enable 8 | KoenZomers.TadoApi.UnitTest 9 | KoenZomers.TadoApi.UnitTest 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | PreserveNewest 20 | 21 | 22 | PreserveNewest 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /Api/Models/Termination.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace KoenZomers.Tado.Api.Models; 4 | 5 | /// 6 | /// Information about when the current state of the Tado device is expected to change 7 | /// 8 | public partial class Termination 9 | { 10 | /// 11 | /// Defines if and what will make the Tado device change its state 12 | /// 13 | [JsonPropertyName("type")] 14 | [JsonConverter(typeof(Converters.DurationModeConverter))] // Ensure this is a System.Text.Json converter 15 | public Enums.DurationModes? CurrentType { get; set; } 16 | 17 | /// 18 | /// Date and time at which the termination mode is expected to change. NULL if CurrentType is Manual. 19 | /// 20 | [JsonPropertyName("projectedExpiry")] 21 | public DateTime? ProjectedExpiry { get; set; } 22 | 23 | /// 24 | /// Date and time at which the termination mode will change. NULL if CurrentType is Manual. 25 | /// 26 | [JsonPropertyName("expiry")] 27 | public DateTime? Expiry { get; set; } 28 | 29 | /// 30 | /// Amount of seconds remaining before the Tado device will change its state. Only set if CurrentType is Timer. 31 | /// 32 | [JsonPropertyName("durationInSeconds")] 33 | public int? DurationInSeconds { get; set; } 34 | } 35 | -------------------------------------------------------------------------------- /Api/Models/User.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace KoenZomers.Tado.Api.Models; 4 | 5 | /// 6 | /// Contains information about a user 7 | /// 8 | public class User 9 | { 10 | /// 11 | /// The full name of the user 12 | /// 13 | [JsonPropertyName("name")] 14 | public string? Name { get; set; } 15 | 16 | /// 17 | /// The email address of the user 18 | /// 19 | [JsonPropertyName("email")] 20 | public string? Email { get; set; } 21 | 22 | /// 23 | /// The username used by the user 24 | /// 25 | [JsonPropertyName("username")] 26 | public string? Username { get; set; } 27 | 28 | /// 29 | /// The unique identifier of the user 30 | /// 31 | [JsonPropertyName("id")] 32 | public string? Id { get; set; } 33 | 34 | /// 35 | /// The list of homes associated with the user 36 | /// 37 | [JsonPropertyName("homes")] 38 | public Home[]? Homes { get; set; } 39 | 40 | /// 41 | /// The locale or language preference of the user 42 | /// 43 | [JsonPropertyName("locale")] 44 | public string? Locale { get; set; } 45 | 46 | /// 47 | /// The list of mobile devices linked to the user 48 | /// 49 | [JsonPropertyName("mobileDevices")] 50 | public MobileDevice.Item[]? MobileDevices { get; set; } 51 | } 52 | -------------------------------------------------------------------------------- /Api/Models/Authentication/DeviceAuthorizationResponse.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace KoenZomers.Tado.Api.Models.Authentication; 4 | 5 | /// 6 | /// Message returned by the Tado API when requesting a device authorization code 7 | /// 8 | public class DeviceAuthorizationResponse 9 | { 10 | /// 11 | /// The device code used to initiate the device authorization flow. 12 | /// 13 | [JsonPropertyName("device_code")] 14 | public string? DeviceCode { get; set; } 15 | 16 | /// 17 | /// The number of seconds before the device code expires. 18 | /// 19 | [JsonPropertyName("expires_in")] 20 | public short? ExpiresIn { get; set; } 21 | 22 | /// 23 | /// The interval in seconds at which the client should poll the token endpoint. 24 | /// 25 | [JsonPropertyName("interval")] 26 | public short? Interval { get; set; } 27 | 28 | /// 29 | /// The user code that the user must enter to authorize the device. 30 | /// 31 | [JsonPropertyName("user_code")] 32 | public string? UserCode { get; set; } 33 | 34 | /// 35 | /// The URI where the user should go to authorize the device. 36 | /// 37 | [JsonPropertyName("verification_uri")] 38 | public string? VerificationUri { get; set; } 39 | 40 | /// 41 | /// The complete URI including the user code for user convenience. 42 | /// 43 | [JsonPropertyName("verification_uri_complete")] 44 | public string? VerificationUriComplete { get; set; } 45 | } 46 | -------------------------------------------------------------------------------- /Api/Models/House.cs: -------------------------------------------------------------------------------- 1 | using KoenZomers.Tado.Api.Entities; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace KoenZomers.Tado.Api.Models; 5 | 6 | /// 7 | /// Contains detailed information about a house 8 | /// 9 | public class House 10 | { 11 | [JsonPropertyName("id")] 12 | public long Id { get; set; } 13 | 14 | [JsonPropertyName("name")] 15 | public string Name { get; set; } 16 | 17 | [JsonPropertyName("dateTimeZone")] 18 | public string DateTimeZone { get; set; } 19 | 20 | [JsonPropertyName("dateCreated")] 21 | public DateTime DateCreated { get; set; } 22 | 23 | [JsonPropertyName("temperatureUnit")] 24 | public string TemperatureUnit { get; set; } 25 | 26 | [JsonPropertyName("installationCompleted")] 27 | public bool InstallationCompleted { get; set; } 28 | 29 | [JsonPropertyName("partner")] 30 | public object Partner { get; set; } 31 | 32 | [JsonPropertyName("simpleSmartScheduleEnabled")] 33 | public bool SimpleSmartScheduleEnabled { get; set; } 34 | 35 | [JsonPropertyName("awayRadiusInMeters")] 36 | public double AwayRadiusInMeters { get; set; } 37 | 38 | [JsonPropertyName("license")] 39 | public string License { get; set; } 40 | 41 | [JsonPropertyName("christmasModeEnabled")] 42 | public bool ChristmasModeEnabled { get; set; } 43 | 44 | [JsonPropertyName("contactDetails")] 45 | public ContactDetails ContactDetails { get; set; } 46 | 47 | [JsonPropertyName("address")] 48 | public Address Address { get; set; } 49 | 50 | [JsonPropertyName("geolocation")] 51 | public Geolocation Geolocation { get; set; } 52 | } -------------------------------------------------------------------------------- /Api/Converters/PowerStatesConverter.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace KoenZomers.Tado.Api.Converters; 5 | 6 | /// 7 | /// Converts the power state returned by the Tado API to the PowerStates enumerator in this project 8 | /// 9 | public class PowerStatesConverter : JsonConverter 10 | { 11 | public override Enums.PowerStates? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) 12 | { 13 | if (reader.TokenType != JsonTokenType.String) 14 | { 15 | return null; 16 | } 17 | 18 | var enumString = reader.GetString(); 19 | if (string.IsNullOrEmpty(enumString)) 20 | { 21 | return null; 22 | } 23 | 24 | return enumString switch 25 | { 26 | "ON" => Enums.PowerStates.On, 27 | "OFF" => Enums.PowerStates.Off, 28 | _ => null 29 | }; 30 | } 31 | 32 | public override void Write(Utf8JsonWriter writer, Enums.PowerStates? value, JsonSerializerOptions options) 33 | { 34 | if (value == null) 35 | { 36 | writer.WriteNullValue(); 37 | return; 38 | } 39 | 40 | switch (value) 41 | { 42 | case Enums.PowerStates.On: 43 | writer.WriteStringValue("ON"); 44 | break; 45 | 46 | case Enums.PowerStates.Off: 47 | writer.WriteStringValue("OFF"); 48 | break; 49 | 50 | default: 51 | writer.WriteNullValue(); 52 | break; 53 | } 54 | } 55 | } -------------------------------------------------------------------------------- /Api/Converters/DeviceTypeConverter.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace KoenZomers.Tado.Api.Converters; 5 | 6 | /// 7 | /// Converts the Tado device type returned by the Tado API to the DeviceTypes enumerator in this project 8 | /// 9 | public class DeviceTypeConverter : JsonConverter 10 | { 11 | public override Enums.DeviceTypes? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) 12 | { 13 | if (reader.TokenType != JsonTokenType.String) 14 | { 15 | return null; 16 | } 17 | 18 | var enumString = reader.GetString(); 19 | if (string.IsNullOrEmpty(enumString)) 20 | { 21 | return null; 22 | } 23 | 24 | return enumString switch 25 | { 26 | "HEATING" => Enums.DeviceTypes.Heating, 27 | "HOT_WATER" => Enums.DeviceTypes.HotWater, 28 | _ => null 29 | }; 30 | } 31 | 32 | public override void Write(Utf8JsonWriter writer, Enums.DeviceTypes? value, JsonSerializerOptions options) 33 | { 34 | if (value == null) 35 | { 36 | writer.WriteNullValue(); 37 | return; 38 | } 39 | 40 | switch (value) 41 | { 42 | case Enums.DeviceTypes.Heating: 43 | writer.WriteStringValue("HEATING"); 44 | break; 45 | 46 | case Enums.DeviceTypes.HotWater: 47 | writer.WriteStringValue("HOT_WATER"); 48 | break; 49 | 50 | default: 51 | writer.WriteNullValue(); 52 | break; 53 | } 54 | } 55 | } -------------------------------------------------------------------------------- /Api/Models/Session.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace KoenZomers.Tado.Api.Models; 4 | 5 | /// 6 | /// Session token coming from an OAuth authentication request 7 | /// 8 | public class Session 9 | { 10 | /// 11 | /// The access token used for authenticated requests 12 | /// 13 | [JsonPropertyName("access_token")] 14 | public string? AccessToken { get; set; } 15 | 16 | private int? expiresIn; 17 | 18 | /// 19 | /// The number of seconds until the access token expires 20 | /// 21 | [JsonPropertyName("expires_in")] 22 | public int? ExpiresIn 23 | { 24 | get => expiresIn; 25 | set 26 | { 27 | expiresIn = value; 28 | Expires = value.HasValue ? DateTime.Now.AddSeconds(value.Value) : null; 29 | } 30 | } 31 | 32 | /// 33 | /// Date and time at which the access token expires 34 | /// 35 | public DateTime? Expires { get; private set; } 36 | 37 | /// 38 | /// The unique token identifier 39 | /// 40 | [JsonPropertyName("jti")] 41 | public string? Jti { get; set; } 42 | 43 | /// 44 | /// The refresh token used to obtain a new access token 45 | /// 46 | [JsonPropertyName("refresh_token")] 47 | public string? RefreshToken { get; set; } 48 | 49 | /// 50 | /// The scope of access granted by the token 51 | /// 52 | [JsonPropertyName("scope")] 53 | public string? Scope { get; set; } 54 | 55 | /// 56 | /// The type of token (typically "Bearer") 57 | /// 58 | [JsonPropertyName("token_type")] 59 | public string? TokenType { get; set; } 60 | } 61 | -------------------------------------------------------------------------------- /Api/Converters/DurationModeConverter.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace KoenZomers.Tado.Api.Converters; 5 | 6 | /// 7 | /// Converts the duration mode type returned by the Tado API to the DurationModes enumerator in this project 8 | /// 9 | public class DurationModeConverter : JsonConverter 10 | { 11 | public override Enums.DurationModes? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) 12 | { 13 | if (reader.TokenType != JsonTokenType.String) 14 | { 15 | return null; 16 | } 17 | 18 | var enumString = reader.GetString(); 19 | if (string.IsNullOrEmpty(enumString)) 20 | { 21 | return null; 22 | } 23 | 24 | return enumString switch 25 | { 26 | "MANUAL" => Enums.DurationModes.UntilNextManualChange, 27 | "TADO_MODE" => Enums.DurationModes.UntilNextTimedEvent, 28 | "TIMER" => Enums.DurationModes.Timer, 29 | _ => null 30 | }; 31 | } 32 | 33 | public override void Write(Utf8JsonWriter writer, Enums.DurationModes? value, JsonSerializerOptions options) 34 | { 35 | if (value == null) 36 | { 37 | writer.WriteNullValue(); 38 | return; 39 | } 40 | 41 | switch (value) 42 | { 43 | case Enums.DurationModes.UntilNextManualChange: 44 | writer.WriteStringValue("MANUAL"); 45 | break; 46 | 47 | case Enums.DurationModes.UntilNextTimedEvent: 48 | writer.WriteStringValue("TADO_MODE"); 49 | break; 50 | 51 | case Enums.DurationModes.Timer: 52 | writer.WriteStringValue("TIMER"); 53 | break; 54 | 55 | default: 56 | writer.WriteNullValue(); 57 | break; 58 | } 59 | } 60 | } -------------------------------------------------------------------------------- /Api/Configuration/Tado.cs: -------------------------------------------------------------------------------- 1 | namespace KoenZomers.Tado.Api.Configuration; 2 | 3 | /// 4 | /// Configuration for the Tado API 5 | /// 6 | public class Tado 7 | { 8 | /// 9 | /// Base URL for the Tado API 10 | /// 11 | public string? BaseUrl { get; set; } 12 | 13 | /// 14 | /// The URL to use for authentication against the Tado API 15 | /// 16 | public string? AuthenticationUrl { get; set; } 17 | 18 | /// 19 | /// The URL to use for retrieving a device code for authentication against the Tado API 20 | /// 21 | public string? TokenUrl { get; set; } 22 | 23 | /// 24 | /// Default timeout for HTTP requests in seconds 25 | /// 26 | public short? DefaultApiTimeoutSeconds { get; set; } 27 | 28 | /// 29 | /// Default user agent for HTTP requests towards Tado. Will be appended with the assembly version number. 30 | /// 31 | public string? UserAgent { get; set; } = "KoenZomers.Tado.Api"; 32 | 33 | /// 34 | /// The client ID to use when connecting to the Tado API 35 | /// 36 | public string? ClientId { get; set; } 37 | 38 | /// 39 | /// Id of the home as registered with Tado 40 | /// 41 | public int? TadoHomeId { get; set; } 42 | 43 | ///// 44 | ///// Id of the zone as registered with Tado 45 | ///// 46 | //public static int ZoneId => int.Parse(ConfigurationManager.AppSettings["TadoZoneId"]); 47 | 48 | ///// 49 | ///// Id of the mobile device as registered with Tado 50 | ///// 51 | //public static int MobileDeviceId => int.Parse(ConfigurationManager.AppSettings["TadoMobileDeviceId"]); 52 | 53 | ///// 54 | ///// Id of the Tado device 55 | ///// 56 | //public static string DeviceId => ConfigurationManager.AppSettings["TadoDeviceId"]; 57 | } 58 | -------------------------------------------------------------------------------- /appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Tado": { 3 | // The custom UserAgent to use in requests towards the Tado API 4 | "UserAgent": "KoenZomers.Tado.Api", 5 | 6 | // The Tado API base URL, typically "https://my.tado.com/api/v2/" 7 | "BaseUrl": "https://my.tado.com/api/v2/", 8 | 9 | // The Tado API authentication URL, typically "https://login.tado.com/oauth2/device_authorize" 10 | "AuthenticationUrl": "https://login.tado.com/oauth2/device_authorize", 11 | 12 | // The Tado API token URL, typically "https://login.tado.com/oauth2/token" 13 | "TokenUrl": "https://login.tado.com/oauth2/token", 14 | 15 | // The Tado API client ID, used for authentication, typically "1bb50063-6b0c-4d11-bd99-387f4a91cc46" 16 | "ClientId": "1bb50063-6b0c-4d11-bd99-387f4a91cc46", 17 | 18 | // The maximum duration a call to the Tado API may take before timing out 19 | "DefaultApiTimeoutSeconds": 30, 20 | 21 | // The Tado API home ID to use for requests 22 | "TadoHomeId": 165523 23 | }, 24 | "Logging": { 25 | "LogLevel": { 26 | // Fatal: Critical errors that cause the application to crash or stop entirely. 27 | // Error: Serious issues that prevent part of the application from functioning correctly. 28 | // Warning: Unexpected events that don't stop the application but may lead to future issues. 29 | // Information: General events that describe the normal flow of the application. 30 | // Debug: Detailed information useful during development and debugging. 31 | // Trace: The most detailed level of logging, capturing every step of execution. 32 | 33 | "Default": "Debug", 34 | "Microsoft.Hosting.Lifetime": "Information", 35 | "Microsoft.AspNetCore": "Warning", 36 | "Microsoft.AspNetCore.Routing": "Warning", 37 | "Microsoft.AspNetCore.Http": "Warning", 38 | "System.Net.Http.HttpClient": "Debug", 39 | 40 | "KoenZomers.Tado.Api.Controllers.Tado": "Information", 41 | "KoenZomers.Tado.Api.Controllers.Http": "Information" 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /UnitTest/BaseTest.cs: -------------------------------------------------------------------------------- 1 | using KoenZomers.Tado.Api.Extensions; 2 | using Microsoft.Extensions.Configuration; 3 | using Microsoft.Extensions.DependencyInjection; 4 | 5 | namespace KoenZomers.Tado.UnitTest; 6 | 7 | /// 8 | /// Base functionality shared by all Unit Tests 9 | /// 10 | public abstract class BaseTest 11 | { 12 | /// 13 | /// The service provider for the application 14 | /// 15 | protected readonly IServiceProvider? ServiceProvider; 16 | 17 | /// 18 | /// Access to the service instance 19 | /// 20 | protected readonly Api.Controllers.Tado? Service; 21 | 22 | /// 23 | /// Access to the service configuration 24 | /// 25 | protected readonly Api.Configuration.Tado? Configuration; 26 | 27 | /// 28 | /// Instantiate the Unit Test by creating the service provider and retrieving the service instance to be tested 29 | /// 30 | public BaseTest() 31 | { 32 | var services = new ServiceCollection(); 33 | 34 | // Ensure the configuration is present 35 | var config = new ConfigurationBuilder() 36 | .SetBasePath(Directory.GetCurrentDirectory()) 37 | .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true) 38 | .AddJsonFile("appsettings.test.json", optional: true, reloadOnChange: true) 39 | .Build(); 40 | services.AddSingleton(config); 41 | 42 | // Add the complete service stack 43 | Configuration = services.AddTadoServices(); 44 | 45 | ServiceProvider = services.BuildServiceProvider(); 46 | 47 | // Retrieve the requested service type to be tested 48 | Service = ServiceProvider.GetService(); 49 | 50 | // Ensure the service is present 51 | if (Service == null) 52 | { 53 | throw new InvalidOperationException($"Failed to create service"); 54 | } 55 | } 56 | } -------------------------------------------------------------------------------- /Api/Models/Authentication/Token.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace KoenZomers.Tado.Api.Models.Authentication; 4 | 5 | /// 6 | /// Contains the response from the Tado API when having performed the device authentication flow 7 | /// 8 | public class Token 9 | { 10 | /// 11 | /// The access token issued by the authorization server 12 | /// 13 | [JsonPropertyName("access_token")] 14 | public string? AccessToken { get; set; } 15 | 16 | /// 17 | /// The lifetime in seconds of the access token. 18 | /// 19 | [JsonPropertyName("expires_in")] 20 | public int? ExpiresIn { get; set; } 21 | 22 | private DateTime? _expiresAt; 23 | /// 24 | /// Date and time at which this token expires 25 | /// 26 | [JsonIgnore] 27 | public DateTime? ExpiresAt => _expiresAt ??= (ExpiresIn.HasValue ? DateTime.Now.AddSeconds(ExpiresIn.Value) : null); 28 | 29 | /// 30 | /// Boolean indicating whether the token is still valid based on the current time and the expiration time 31 | /// 32 | [JsonIgnore] 33 | public bool IsValid => ExpiresAt.HasValue && ExpiresAt.Value >= DateTime.Now; 34 | 35 | /// 36 | /// The token that can be used to obtain new access tokens using a refresh flow 37 | /// 38 | [JsonPropertyName("refresh_token")] 39 | public string? RefreshToken { get; set; } 40 | 41 | /// 42 | /// The scope of the access token 43 | /// 44 | [JsonPropertyName("scope")] 45 | public string? Scope { get; set; } 46 | 47 | /// 48 | /// The type of the token issued 49 | /// 50 | [JsonPropertyName("token_type")] 51 | public string? TokenType { get; set; } 52 | 53 | /// 54 | /// The identifier of the user associated with the token 55 | /// 56 | [JsonPropertyName("userId")] 57 | public string? UserId { get; set; } 58 | } 59 | -------------------------------------------------------------------------------- /Api/.NET API.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net9.0;net10.0 5 | enable 6 | enable 7 | KoenZomers.Tado.Api 8 | KoenZomers.Tado.Api 9 | true 10 | 0.7.0.0 11 | Koen Zomers 12 | API in .NET9 and .NET10 to communicate with a Tado home heating/cooling system 13 | https://github.com/KoenZomers/TadoApi 14 | true 15 | ..\KoenZomers.Tado.Api.snk 16 | - Added target .NET10 and improved new device code grant flow with cancellation tokens 17 | 18 | Apache-2.0 19 | README.md 20 | false 21 | 22 | 23 | 24 | ..\KoenZomers.Tado.Api.xml 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /Api/Helpers/QueryStringBuilder.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | 3 | namespace KoenZomers.Tado.Api.Helpers; 4 | 5 | public class QueryStringBuilder 6 | { 7 | private readonly Dictionary _parameters = []; 8 | 9 | public bool HasKeys => _parameters.Count > 0; 10 | 11 | public char? StartCharacter { get; set; } = null; 12 | 13 | public char SeperatorCharacter { get; set; } = '&'; 14 | 15 | public char KeyValueJoinCharacter { get; set; } = '='; 16 | 17 | public string? this[string key] 18 | { 19 | get => _parameters.ContainsKey(key) ? _parameters[key] : null; 20 | set => _parameters[key] = value; 21 | } 22 | 23 | public string[] Keys => _parameters.Keys.ToArray(); 24 | 25 | public void Clear() 26 | { 27 | _parameters.Clear(); 28 | } 29 | 30 | public bool ContainsKey(string key) 31 | { 32 | return _parameters.ContainsKey(key); 33 | } 34 | 35 | public void Add(string key, string value) 36 | { 37 | _parameters[key] = value; 38 | } 39 | 40 | public void Remove(string key) 41 | { 42 | _parameters.Remove(key); 43 | } 44 | 45 | public override string ToString() 46 | { 47 | var stringBuilder = new StringBuilder(); 48 | foreach (var keyValuePair in _parameters) 49 | { 50 | if (stringBuilder.Length == 0) 51 | { 52 | var startCharacter = StartCharacter; 53 | if ((startCharacter.HasValue ? startCharacter.GetValueOrDefault() : new int?()).HasValue) 54 | stringBuilder.Append(StartCharacter); 55 | } 56 | if (stringBuilder.Length > 0) 57 | { 58 | int num = stringBuilder[stringBuilder.Length - 1]; 59 | char? startCharacter = StartCharacter; 60 | if ((num != (int)startCharacter.GetValueOrDefault() ? 1 : (!startCharacter.HasValue ? 1 : 0)) != 0) 61 | stringBuilder.Append(SeperatorCharacter); 62 | } 63 | stringBuilder.Append(keyValuePair.Key); 64 | stringBuilder.Append('='); 65 | stringBuilder.Append(Uri.EscapeDataString(keyValuePair.Value)); 66 | } 67 | return stringBuilder.ToString(); 68 | } 69 | } -------------------------------------------------------------------------------- /Api/Models/Device.cs: -------------------------------------------------------------------------------- 1 | using KoenZomers.Tado.Api.Entities; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace KoenZomers.Tado.Api.Models; 5 | 6 | /// 7 | /// Information about one Tado device 8 | /// 9 | public class Device 10 | { 11 | /// 12 | /// The type of the device (e.g., SMART_THERMOSTAT) 13 | /// 14 | [JsonPropertyName("deviceType")] 15 | public string? DeviceType { get; set; } 16 | 17 | /// 18 | /// The full serial number of the device 19 | /// 20 | [JsonPropertyName("serialNo")] 21 | public string? SerialNo { get; set; } 22 | 23 | /// 24 | /// The short version of the device's serial number 25 | /// 26 | [JsonPropertyName("shortSerialNo")] 27 | public string? ShortSerialNo { get; set; } 28 | 29 | /// 30 | /// The current firmware version installed on the device 31 | /// 32 | [JsonPropertyName("currentFwVersion")] 33 | public string? CurrentFwVersion { get; set; } 34 | 35 | /// 36 | /// The current connection state of the device 37 | /// 38 | [JsonPropertyName("connectionState")] 39 | public ConnectionState? ConnectionState { get; set; } 40 | 41 | /// 42 | /// The characteristics of the device 43 | /// 44 | [JsonPropertyName("characteristics")] 45 | public Characteristics? Characteristics { get; set; } 46 | 47 | /// 48 | /// The list of duties assigned to the device 49 | /// 50 | [JsonPropertyName("duties")] 51 | public string[]? Duties { get; set; } 52 | 53 | /// 54 | /// The mounting state of the device 55 | /// 56 | [JsonPropertyName("mountingState")] 57 | public MountingState? MountingState { get; set; } 58 | 59 | /// 60 | /// The battery state of the device (e.g., NORMAL, LOW) 61 | /// 62 | [JsonPropertyName("batteryState")] 63 | public string? BatteryState { get; set; } 64 | 65 | /// 66 | /// Indicates if child lock is enabled or disabled on the Tado device 67 | /// 68 | [JsonPropertyName("childLockEnabled")] 69 | public bool? ChildLockEnabled { get; set; } 70 | } 71 | -------------------------------------------------------------------------------- /Api/Models/Zone.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace KoenZomers.Tado.Api.Models; 4 | 5 | /// 6 | /// Information about one zone 7 | /// 8 | public class Zone 9 | { 10 | /// 11 | /// The unique identifier of the zone 12 | /// 13 | [JsonPropertyName("id")] 14 | public long? Id { get; set; } 15 | 16 | /// 17 | /// The name of the zone 18 | /// 19 | [JsonPropertyName("name")] 20 | public string? Name { get; set; } 21 | 22 | /// 23 | /// The current type of the zone (e.g., HEATING, HOT_WATER) 24 | /// 25 | [JsonPropertyName("type")] 26 | public string? CurrentType { get; set; } 27 | 28 | /// 29 | /// The date and time when the zone was created 30 | /// 31 | [JsonPropertyName("dateCreated")] 32 | public DateTime? DateCreated { get; set; } 33 | 34 | /// 35 | /// The types of devices associated with the zone 36 | /// 37 | [JsonPropertyName("deviceTypes")] 38 | public string[]? DeviceTypes { get; set; } 39 | 40 | /// 41 | /// The list of devices in the zone 42 | /// 43 | [JsonPropertyName("devices")] 44 | public Device[]? Devices { get; set; } 45 | 46 | /// 47 | /// Indicates whether a report is available for the zone 48 | /// 49 | [JsonPropertyName("reportAvailable")] 50 | public bool? ReportAvailable { get; set; } 51 | 52 | /// 53 | /// Indicates whether the zone supports the Dazzle feature 54 | /// 55 | [JsonPropertyName("supportsDazzle")] 56 | public bool? SupportsDazzle { get; set; } 57 | 58 | /// 59 | /// Indicates whether the Dazzle feature is enabled 60 | /// 61 | [JsonPropertyName("dazzleEnabled")] 62 | public bool? DazzleEnabled { get; set; } 63 | 64 | /// 65 | /// The current Dazzle mode configuration 66 | /// 67 | [JsonPropertyName("dazzleMode")] 68 | public DazzleMode? DazzleMode { get; set; } 69 | 70 | /// 71 | /// The open window detection settings for the zone 72 | /// 73 | [JsonPropertyName("openWindowDetection")] 74 | public OpenWindowDetection? OpenWindowDetection { get; set; } 75 | } 76 | -------------------------------------------------------------------------------- /Api/Extensions/TadoService.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Configuration; 2 | using Microsoft.Extensions.DependencyInjection; 3 | 4 | namespace KoenZomers.Tado.Api.Extensions; 5 | 6 | /// 7 | /// Tado Service Instantiation for the framework 8 | /// 9 | public static class PowerManagerService 10 | { 11 | /// 12 | /// Instantiates the PowerManager Framework components 13 | /// 14 | /// Base path where the application files reside 15 | public static Configuration.Tado AddTadoServices(this IServiceCollection services) 16 | { 17 | ArgumentNullException.ThrowIfNull(services); 18 | 19 | Console.WriteLine("- Adding configuration"); 20 | var serviceProvider = services.BuildServiceProvider(); 21 | var configBuilder = serviceProvider.GetRequiredService(); 22 | 23 | // Retrieve the PowerManager configuration section from the config file 24 | var configSection = configBuilder.GetSection("Tado"); 25 | var config = configSection.Get() ?? throw new InvalidOperationException("Failed to create configuration"); 26 | 27 | // Add the services 28 | services.Configure(configSection) 29 | .AddTadoControllers() 30 | .AddTadoHttpClients(config); 31 | 32 | return config; 33 | } 34 | 35 | /// 36 | /// Adds named HttpClient instances 37 | /// 38 | public static IServiceCollection AddTadoHttpClients(this IServiceCollection services, Configuration.Tado configuration) 39 | { 40 | // Define the User Agent to use in the HttpClient 41 | var userAgent = $"{configuration.UserAgent}/{System.Reflection.Assembly.GetExecutingAssembly()?.GetName().Version?.ToString(4)}"; 42 | 43 | services.AddHttpClient("Tado", client => { client.DefaultRequestHeaders.UserAgent.ParseAdd(userAgent); }); 44 | 45 | return services; 46 | } 47 | 48 | /// 49 | /// Adds the controllers to the Tado framework 50 | /// 51 | public static IServiceCollection AddTadoControllers(this IServiceCollection services) 52 | { 53 | Console.WriteLine("- Adding controllers"); 54 | 55 | services.AddSingleton(); 56 | services.AddSingleton(); 57 | 58 | return services; 59 | } 60 | } -------------------------------------------------------------------------------- /Api/Models/State.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | using KoenZomers.Tado.Api.Entities; 3 | 4 | namespace KoenZomers.Tado.Api.Models; 5 | 6 | /// 7 | /// State of a specific zone 8 | /// 9 | public class State 10 | { 11 | /// 12 | /// The current Tado mode (e.g., HOME, AWAY) 13 | /// 14 | [JsonPropertyName("tadoMode")] 15 | public string? TadoMode { get; set; } 16 | 17 | /// 18 | /// Indicates whether geolocation override is active 19 | /// 20 | [JsonPropertyName("geolocationOverride")] 21 | public bool? GeolocationOverride { get; set; } 22 | 23 | /// 24 | /// The time when geolocation override will be disabled 25 | /// 26 | [JsonPropertyName("geolocationOverrideDisableTime")] 27 | public object? GeolocationOverrideDisableTime { get; set; } 28 | 29 | /// 30 | /// Preparation state information (if any) 31 | /// 32 | [JsonPropertyName("preparation")] 33 | public object? Preparation { get; set; } 34 | 35 | /// 36 | /// The current setting applied to the zone 37 | /// 38 | [JsonPropertyName("setting")] 39 | public Setting? Setting { get; set; } 40 | 41 | /// 42 | /// The type of overlay currently active 43 | /// 44 | [JsonPropertyName("overlayType")] 45 | public string? OverlayType { get; set; } 46 | 47 | /// 48 | /// The overlay configuration currently applied 49 | /// 50 | [JsonPropertyName("overlay")] 51 | public Overlay? Overlay { get; set; } 52 | 53 | /// 54 | /// Information about an open window event (if any) 55 | /// 56 | [JsonPropertyName("openWindow")] 57 | public object? OpenWindow { get; set; } 58 | 59 | /// 60 | /// Indicates whether an open window has been detected 61 | /// 62 | [JsonPropertyName("openWindowDetected")] 63 | public bool? OpenWindowDetected { get; set; } 64 | 65 | /// 66 | /// The link state of the zone 67 | /// 68 | [JsonPropertyName("link")] 69 | public Link? Link { get; set; } 70 | 71 | /// 72 | /// Activity data points such as heating power 73 | /// 74 | [JsonPropertyName("activityDataPoints")] 75 | public ActivityDataPoints? ActivityDataPoints { get; set; } 76 | 77 | /// 78 | /// Sensor data points such as temperature and humidity 79 | /// 80 | [JsonPropertyName("sensorDataPoints")] 81 | public SensorDataPoints? SensorDataPoints { get; set; } 82 | } 83 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /KoenZomers.Tado.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.14.36212.18 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{E74CD4FE-CB4A-4968-B282-C0118BBF63DA}" 7 | ProjectSection(SolutionItems) = preProject 8 | appsettings.json = appsettings.json 9 | appsettings.test.json = appsettings.test.json 10 | KoenZomers.Tado.Api.snk = KoenZomers.Tado.Api.snk 11 | EndProjectSection 12 | EndProject 13 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Documentation", "Documentation", "{12C92BB7-7D1A-49F5-AD96-2101FEE61C8E}" 14 | ProjectSection(SolutionItems) = preProject 15 | README.md = README.md 16 | EndProjectSection 17 | EndProject 18 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Images", "Images", "{5D858129-BB53-446B-B8B8-C83D383F7F1C}" 19 | EndProject 20 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = ".NET API", "Api\.NET API.csproj", "{4F94C11E-80E6-46A9-AB84-C2A257564681}" 21 | EndProject 22 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnitTest", "UnitTest\UnitTest.csproj", "{BA731FD4-9AD6-426B-A069-42C6EC144AC2}" 23 | EndProject 24 | Global 25 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 26 | Debug|Any CPU = Debug|Any CPU 27 | Debug|x86 = Debug|x86 28 | Release|Any CPU = Release|Any CPU 29 | Release|x86 = Release|x86 30 | EndGlobalSection 31 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 32 | {4F94C11E-80E6-46A9-AB84-C2A257564681}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 33 | {4F94C11E-80E6-46A9-AB84-C2A257564681}.Debug|Any CPU.Build.0 = Debug|Any CPU 34 | {4F94C11E-80E6-46A9-AB84-C2A257564681}.Debug|x86.ActiveCfg = Debug|Any CPU 35 | {4F94C11E-80E6-46A9-AB84-C2A257564681}.Debug|x86.Build.0 = Debug|Any CPU 36 | {4F94C11E-80E6-46A9-AB84-C2A257564681}.Release|Any CPU.ActiveCfg = Release|Any CPU 37 | {4F94C11E-80E6-46A9-AB84-C2A257564681}.Release|Any CPU.Build.0 = Release|Any CPU 38 | {4F94C11E-80E6-46A9-AB84-C2A257564681}.Release|x86.ActiveCfg = Release|Any CPU 39 | {4F94C11E-80E6-46A9-AB84-C2A257564681}.Release|x86.Build.0 = Release|Any CPU 40 | {BA731FD4-9AD6-426B-A069-42C6EC144AC2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 41 | {BA731FD4-9AD6-426B-A069-42C6EC144AC2}.Debug|Any CPU.Build.0 = Debug|Any CPU 42 | {BA731FD4-9AD6-426B-A069-42C6EC144AC2}.Debug|x86.ActiveCfg = Debug|Any CPU 43 | {BA731FD4-9AD6-426B-A069-42C6EC144AC2}.Debug|x86.Build.0 = Debug|Any CPU 44 | {BA731FD4-9AD6-426B-A069-42C6EC144AC2}.Release|Any CPU.ActiveCfg = Release|Any CPU 45 | {BA731FD4-9AD6-426B-A069-42C6EC144AC2}.Release|Any CPU.Build.0 = Release|Any CPU 46 | {BA731FD4-9AD6-426B-A069-42C6EC144AC2}.Release|x86.ActiveCfg = Release|Any CPU 47 | {BA731FD4-9AD6-426B-A069-42C6EC144AC2}.Release|x86.Build.0 = Release|Any CPU 48 | EndGlobalSection 49 | GlobalSection(SolutionProperties) = preSolution 50 | HideSolutionNode = FALSE 51 | EndGlobalSection 52 | GlobalSection(NestedProjects) = preSolution 53 | {5D858129-BB53-446B-B8B8-C83D383F7F1C} = {12C92BB7-7D1A-49F5-AD96-2101FEE61C8E} 54 | EndGlobalSection 55 | GlobalSection(ExtensibilityGlobals) = postSolution 56 | SolutionGuid = {A83E856C-75F3-420C-8E18-DC86F6B73C35} 57 | EndGlobalSection 58 | EndGlobal 59 | -------------------------------------------------------------------------------- /UnitTest/AuthenticationTest.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | 3 | namespace KoenZomers.Tado.UnitTest; 4 | 5 | /// 6 | /// Unit Tests to validate authenticating against the Tado API 7 | /// 8 | [TestClass] 9 | public class AuthenticationTest : BaseTest 10 | { 11 | /// 12 | /// Test being able to retrieve an authorization URL 13 | /// 14 | /// 15 | [TestMethod] 16 | public async Task GetDeviceCodeAuthenticationTest() 17 | { 18 | if (Service is null) Assert.Fail("Service not available"); 19 | 20 | var authenticationRequest = await Service.GetDeviceCodeAuthentication(CancellationToken.None); 21 | 22 | Assert.IsNotNull(authenticationRequest, "Failed to instantiate device authentication flow"); 23 | } 24 | 25 | /// 26 | /// Test requesting a device authorization code and waiting for it to be completed 27 | /// 28 | /// 29 | [TestMethod] 30 | public async Task WaitForDeviceCodeAuthenticationToCompleteTest() 31 | { 32 | if (Service is null) Assert.Fail("Service not available"); 33 | 34 | var authenticationRequest = await Service.GetDeviceCodeAuthentication(CancellationToken.None); 35 | 36 | Assert.IsNotNull(authenticationRequest, "Failed to instantiate device authentication flow"); 37 | 38 | Debug.WriteLine($"URL for authentication: {authenticationRequest.VerificationUriComplete}"); 39 | 40 | var tokenResponse = await Service.WaitForDeviceCodeAuthenticationToComplete(authenticationRequest, CancellationToken.None); 41 | 42 | Assert.IsNotNull(tokenResponse, "Failed to complete device authentication flow"); 43 | 44 | Debug.WriteLine($"Access token: {tokenResponse.AccessToken}"); 45 | Debug.WriteLine($"Refresh token: {tokenResponse.RefreshToken}"); 46 | } 47 | 48 | /// 49 | /// Test requesting an access token with a refresh token 50 | /// 51 | /// 52 | [TestMethod] 53 | public async Task GetAccessTokenWithRefreshTokenTest() 54 | { 55 | if (Service is null) Assert.Fail("Service not available"); 56 | 57 | var tokenResponse = await Service.GetAccessTokenWithRefreshToken("xxx", CancellationToken.None); 58 | 59 | Assert.IsNotNull(tokenResponse, "Failed to retrieve access token with refresh token"); 60 | } 61 | 62 | /// 63 | /// Test requesting the Me endpoint from Tado 64 | /// 65 | /// 66 | [TestMethod] 67 | public async Task GetMeTest() 68 | { 69 | if (Service is null) Assert.Fail("Service not available"); 70 | 71 | var authenticationRequest = await Service.GetDeviceCodeAuthentication(CancellationToken.None); 72 | 73 | Assert.IsNotNull(authenticationRequest, "Failed to instantiate device authentication flow"); 74 | 75 | Debug.WriteLine($"URL for authentication: {authenticationRequest.VerificationUriComplete}"); 76 | 77 | var tokenResponse = await Service.WaitForDeviceCodeAuthenticationToComplete(authenticationRequest, CancellationToken.None); 78 | 79 | Assert.IsNotNull(tokenResponse, "Failed to complete device authentication flow"); 80 | 81 | Service.Authenticate(tokenResponse); 82 | 83 | var devices = await Service.GetDevices(Configuration.TadoHomeId.Value); 84 | 85 | var me = await Service.SayHi("xxxx"); 86 | 87 | Assert.IsNotNull(me, "Failed to retrieve information from Tado"); 88 | } 89 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.sln.docstates 8 | 9 | # Build results 10 | [Dd]ebug/ 11 | [Dd]ebugPublic/ 12 | [Rr]elease/ 13 | x64/ 14 | build/ 15 | bld/ 16 | [Bb]in/ 17 | [Oo]bj/ 18 | 19 | # Roslyn cache directories 20 | *.ide/ 21 | 22 | # MSTest test Results 23 | [Tt]est[Rr]esult*/ 24 | [Bb]uild[Ll]og.* 25 | 26 | #NUNIT 27 | *.VisualState.xml 28 | TestResult.xml 29 | 30 | # Build Results of an ATL Project 31 | [Dd]ebugPS/ 32 | [Rr]eleasePS/ 33 | dlldata.c 34 | 35 | *_i.c 36 | *_p.c 37 | *_i.h 38 | *.ilk 39 | *.meta 40 | *.obj 41 | *.pch 42 | *.pdb 43 | *.pgc 44 | *.pgd 45 | *.rsp 46 | *.sbr 47 | *.tlb 48 | *.tli 49 | *.tlh 50 | *.tmp 51 | *.tmp_proj 52 | *.log 53 | *.vspscc 54 | *.vssscc 55 | .builds 56 | *.pidb 57 | *.svclog 58 | *.scc 59 | 60 | # Chutzpah Test files 61 | _Chutzpah* 62 | 63 | # Visual C++ cache files 64 | ipch/ 65 | *.aps 66 | *.ncb 67 | *.opensdf 68 | *.sdf 69 | *.cachefile 70 | 71 | # Visual Studio profiler 72 | *.psess 73 | *.vsp 74 | *.vspx 75 | 76 | # TFS 2012 Local Workspace 77 | $tf/ 78 | 79 | # Guidance Automation Toolkit 80 | *.gpState 81 | 82 | # ReSharper is a .NET coding add-in 83 | _ReSharper*/ 84 | *.[Rr]e[Ss]harper 85 | *.DotSettings.user 86 | 87 | # JustCode is a .NET coding addin-in 88 | .JustCode 89 | 90 | # TeamCity is a build add-in 91 | _TeamCity* 92 | 93 | # DotCover is a Code Coverage Tool 94 | *.dotCover 95 | 96 | # NCrunch 97 | _NCrunch_* 98 | .*crunch*.local.xml 99 | 100 | # MightyMoose 101 | *.mm.* 102 | AutoTest.Net/ 103 | 104 | # Web workbench (sass) 105 | .sass-cache/ 106 | 107 | # Installshield output folder 108 | [Ee]xpress/ 109 | 110 | # DocProject is a documentation generator add-in 111 | DocProject/buildhelp/ 112 | DocProject/Help/*.HxT 113 | DocProject/Help/*.HxC 114 | DocProject/Help/*.hhc 115 | DocProject/Help/*.hhk 116 | DocProject/Help/*.hhp 117 | DocProject/Help/Html2 118 | DocProject/Help/html 119 | 120 | # Click-Once directory 121 | publish/ 122 | 123 | # Publish Web Output 124 | *.[Pp]ublish.xml 125 | *.azurePubxml 126 | ## TODO: Comment the next line if you want to checkin your 127 | ## web deploy settings but do note that will include unencrypted 128 | ## passwords 129 | #*.pubxml 130 | 131 | # NuGet Packages Directory 132 | packages/* 133 | ## TODO: If the tool you use requires repositories.config 134 | ## uncomment the next line 135 | #!packages/repositories.config 136 | 137 | # Enable "build/" folder in the NuGet Packages folder since 138 | # NuGet packages use it for MSBuild targets. 139 | # This line needs to be after the ignore of the build folder 140 | # (and the packages folder if the line above has been uncommented) 141 | !packages/build/ 142 | 143 | # Windows Azure Build Output 144 | csx/ 145 | *.build.csdef 146 | 147 | # Windows Store app package directory 148 | AppPackages/ 149 | 150 | # Others 151 | sql/ 152 | *.Cache 153 | ClientBin/ 154 | [Ss]tyle[Cc]op.* 155 | ~$* 156 | *~ 157 | *.dbmdl 158 | *.dbproj.schemaview 159 | *.pfx 160 | *.publishsettings 161 | node_modules/ 162 | 163 | # RIA/Silverlight projects 164 | Generated_Code/ 165 | 166 | # Backup & report files from converting an old project file 167 | # to a newer Visual Studio version. Backup files are not needed, 168 | # because we have git ;-) 169 | _UpgradeReport_Files/ 170 | Backup*/ 171 | UpgradeLog*.XML 172 | UpgradeLog*.htm 173 | 174 | # SQL Server files 175 | *.mdf 176 | *.ldf 177 | 178 | # Business Intelligence projects 179 | *.rdl.data 180 | *.bim.layout 181 | *.bim_*.settings 182 | 183 | # Microsoft Fakes 184 | FakesAssemblies/ 185 | 186 | # LightSwitch generated files 187 | GeneratedArtifacts/ 188 | _Pvt_Extensions/ 189 | ModelManifest.xml 190 | 191 | App.config 192 | /.vs 193 | -------------------------------------------------------------------------------- /Api/Exceptions/RequestThrottledException.cs: -------------------------------------------------------------------------------- 1 | namespace KoenZomers.Tado.Api.Exceptions; 2 | 3 | /// 4 | /// Exception thrown when a request is being throttled (HTTP 429 response) 5 | /// 6 | public class RequestThrottledException : Exception 7 | { 8 | /// 9 | /// Uri that was called 10 | /// 11 | public Uri Uri { get; private set; } 12 | 13 | /// 14 | /// The rate limit policy name (e.g., "perday") 15 | /// 16 | public string? RateLimitPolicyName { get; private set; } 17 | 18 | /// 19 | /// The quota limit for the rate limit policy (e.g., 20000 requests per day) 20 | /// 21 | public int? RateLimitQuota { get; private set; } 22 | 23 | /// 24 | /// The time window for the rate limit policy in seconds (e.g., 86400 for daily) 25 | /// 26 | public int? RateLimitWindow { get; private set; } 27 | 28 | /// 29 | /// The remaining requests allowed in the current window 30 | /// 31 | public int? RemainingRequests { get; private set; } 32 | 33 | /// 34 | /// The time in seconds until the rate limit resets 35 | /// 36 | public int? ResetTimeSeconds { get; private set; } 37 | 38 | /// 39 | /// Instantiates a new instance of the class. 40 | /// 41 | /// Uri that was being called 42 | /// Http Response Message. Optional. 43 | /// Exception raised while making the request. Optional. 44 | public RequestThrottledException(Uri uri, HttpResponseMessage? httpResponseMessage = null, Exception? innerException = null) : base($"The request to {uri} failed because of throttling", innerException) 45 | { 46 | Uri = uri; 47 | 48 | if (httpResponseMessage != null) 49 | { 50 | ParseRateLimitHeaders(httpResponseMessage); 51 | } 52 | } 53 | 54 | /// 55 | /// Parses the RateLimit-Policy and RateLimit headers from the HTTP response 56 | /// 57 | /// The HTTP response message containing the headers 58 | private void ParseRateLimitHeaders(HttpResponseMessage httpResponseMessage) 59 | { 60 | // Parse RateLimit-Policy header: "perday";q=20000;w=86400 61 | if (httpResponseMessage.Headers.TryGetValues("RateLimit-Policy", out var policyValues)) 62 | { 63 | var policyHeader = policyValues.FirstOrDefault(); 64 | if (!string.IsNullOrEmpty(policyHeader)) 65 | { 66 | ParseRateLimitPolicy(policyHeader); 67 | } 68 | } 69 | 70 | // Parse RateLimit header: "perday";r=0;t=7082 71 | if (httpResponseMessage.Headers.TryGetValues("RateLimit", out var rateLimitValues)) 72 | { 73 | var rateLimitHeader = rateLimitValues.FirstOrDefault(); 74 | if (!string.IsNullOrEmpty(rateLimitHeader)) 75 | { 76 | ParseRateLimit(rateLimitHeader); 77 | } 78 | } 79 | } 80 | 81 | /// 82 | /// Parses the RateLimit-Policy header to extract policy name, quota, and window 83 | /// Format: "policy_name";q=quota;w=window_seconds 84 | /// 85 | /// The RateLimit-Policy header value 86 | private void ParseRateLimitPolicy(string policyHeader) 87 | { 88 | var parts = policyHeader.Split(';'); 89 | 90 | // First part is the policy name (remove quotes) 91 | if (parts.Length > 0) 92 | { 93 | RateLimitPolicyName = parts[0].Trim('"'); 94 | } 95 | 96 | // Parse remaining parts for quota (q) and window (w) 97 | foreach (var part in parts.Skip(1)) 98 | { 99 | var trimmedPart = part.Trim(); 100 | if (trimmedPart.StartsWith("q=") && int.TryParse(trimmedPart.Substring(2), out var quota)) 101 | { 102 | RateLimitQuota = quota; 103 | } 104 | else if (trimmedPart.StartsWith("w=") && int.TryParse(trimmedPart.Substring(2), out var window)) 105 | { 106 | RateLimitWindow = window; 107 | } 108 | } 109 | } 110 | 111 | /// 112 | /// Parses the RateLimit header to extract remaining requests and reset time 113 | /// Format: "policy_name";r=remaining;t=reset_seconds 114 | /// 115 | /// The RateLimit header value 116 | private void ParseRateLimit(string rateLimitHeader) 117 | { 118 | var parts = rateLimitHeader.Split(';'); 119 | 120 | // Parse parts for remaining requests (r) and reset time (t) 121 | foreach (var part in parts.Skip(1)) // Skip policy name 122 | { 123 | var trimmedPart = part.Trim(); 124 | if (trimmedPart.StartsWith("r=") && int.TryParse(trimmedPart.Substring(2), out var remaining)) 125 | { 126 | RemainingRequests = remaining; 127 | } 128 | else if (trimmedPart.StartsWith("t=") && int.TryParse(trimmedPart.Substring(2), out var resetTime)) 129 | { 130 | ResetTimeSeconds = resetTime; 131 | } 132 | } 133 | } 134 | } -------------------------------------------------------------------------------- /UnitTest/DataRetrievalTest.cs: -------------------------------------------------------------------------------- 1 | using KoenZomers.Tado.Api.Helpers; 2 | using Microsoft.VisualStudio.TestTools.UnitTesting; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | namespace KoenZomers.Tado.UnitTest; 7 | 8 | /// 9 | /// Unit Tests for retrieving data from the Tado API 10 | /// 11 | [TestClass] 12 | public class DataRetrievalTest : BaseTest 13 | { 14 | /// 15 | /// Tado Session to use for all tests 16 | /// 17 | private static Session session; 18 | 19 | /// 20 | /// Sets up a session to be used by all test methods in this class 21 | /// 22 | [ClassInitialize] 23 | public static async Task ClassInitialize(TestContext testContext) 24 | { 25 | session = new Session(Username, Password); 26 | await session.Authenticate(); 27 | } 28 | 29 | /// 30 | /// Cleans up the session that was used by all test methods in this class 31 | /// 32 | [ClassCleanup] 33 | public static void ClassCleanup() 34 | { 35 | session.Dispose(); 36 | } 37 | 38 | /// 39 | /// Test if information about the current Tado account can be retrieved 40 | /// 41 | [TestMethod] 42 | public async Task GetMeTest() 43 | { 44 | var response = await session.GetMe(); 45 | Assert.IsNotNull(response, "Failed to retrieve information about the current user"); 46 | } 47 | 48 | /// 49 | /// Test if the zones can be retrieved 50 | /// 51 | [TestMethod] 52 | public async Task GetZonesTest() 53 | { 54 | var response = await session.GetZones(HomeId); 55 | Assert.IsNotNull(response, "Failed to retrieve information about the zones"); 56 | } 57 | 58 | /// 59 | /// Test if the Tado devices can be retrieved 60 | /// 61 | [TestMethod] 62 | public async Task GetDevicesTest() 63 | { 64 | var response = await session.GetDevices(HomeId); 65 | Assert.IsNotNull(response, "Failed to retrieve information about the Tado devices"); 66 | } 67 | 68 | /// 69 | /// Test if the mobile devices can be retrieved 70 | /// 71 | [TestMethod] 72 | public async Task GetMobileDevicesTest() 73 | { 74 | var response = await session.GetMobileDevices(HomeId); 75 | Assert.IsNotNull(response, "Failed to retrieve information about the mobile devices"); 76 | } 77 | 78 | /// 79 | /// Test if the installations can be retrieved 80 | /// 81 | [TestMethod] 82 | public async Task GetInstallationsTest() 83 | { 84 | var response = await session.GetInstallations(HomeId); 85 | Assert.IsNotNull(response, "Failed to retrieve information about the installations"); 86 | } 87 | 88 | /// 89 | /// Test if the state of the zone can be retrieved 90 | /// 91 | [TestMethod] 92 | public async Task GetZoneStateTest() 93 | { 94 | var response = await session.GetZoneState(HomeId, ZoneId); 95 | Assert.IsNotNull(response, "Failed to retrieve information about the state of the zone"); 96 | } 97 | 98 | /// 99 | /// Test if the home state can be retrieved 100 | /// 101 | [TestMethod] 102 | public async Task GetHomeStateTest() 103 | { 104 | var response = await session.GetHomeState(HomeId); 105 | Assert.IsNotNull(response, "Failed to retrieve information about the home state"); 106 | } 107 | 108 | /// 109 | /// Test if the summarized state of a zone can be retrieved 110 | /// 111 | [TestMethod] 112 | public async Task GetSummarizedZoneStateTest() 113 | { 114 | var response = await session.GetSummarizedZoneState(HomeId, ZoneId); 115 | Assert.IsNotNull(response, "Failed to retrieve information about the summarized state of the zone"); 116 | } 117 | 118 | /// 119 | /// Test if the current weahter at the house can be retrieved 120 | /// 121 | [TestMethod] 122 | public async Task GetWeatherTest() 123 | { 124 | var response = await session.GetWeather(HomeId); 125 | Assert.IsNotNull(response, "Failed to retrieve information about the current weather at the house"); 126 | } 127 | 128 | /// 129 | /// Test if the house details can be retrieved 130 | /// 131 | [TestMethod] 132 | public async Task GetHomeTest() 133 | { 134 | var response = await session.GetHome(HomeId); 135 | 136 | Assert.IsNotNull(response, "Failed to retrieve information about the house"); 137 | } 138 | 139 | /// 140 | /// Test if the users with access to a house can be retrieved 141 | /// 142 | [TestMethod] 143 | public async Task GetUsersTest() 144 | { 145 | var response = await session.GetUsers(HomeId); 146 | 147 | Assert.IsNotNull(response, "Failed to retrieve information about the users with access to a house"); 148 | } 149 | 150 | /// 151 | /// Test if the settings of a mobile device registered to a house can be retrieved 152 | /// 153 | [TestMethod] 154 | public async Task GetMobileDeviceSettingsTest() 155 | { 156 | var response = await session.GetMobileDeviceSettings(HomeId, MobileDeviceId); 157 | 158 | Assert.IsNotNull(response, "Failed to retrieve information about the settings of a mobile device registered to a house"); 159 | } 160 | 161 | /// 162 | /// Test if the capabilities of a zone registered to a house can be retrieved 163 | /// 164 | [TestMethod] 165 | public async Task GetZoneCapabilitiesTest() 166 | { 167 | var response = await session.GetZoneCapabilities(HomeId, ZoneId); 168 | 169 | Assert.IsNotNull(response, "Failed to retrieve information about the capabilities of a zone"); 170 | } 171 | 172 | /// 173 | /// Test if the early start setting of a zone registered to a house can be retrieved 174 | /// 175 | [TestMethod] 176 | public async Task GetEarlyStartTest() 177 | { 178 | var response = await session.GetEarlyStart(HomeId, ZoneId); 179 | 180 | Assert.IsNotNull(response, "Failed to retrieve information about the early start setting of a zone"); 181 | } 182 | 183 | /// 184 | /// Test to show how IsOpenWindowDetected can be used on a zone's state. 185 | /// 186 | [TestMethod] 187 | public async Task IsOpenWindowDetectedTest() 188 | { 189 | Entities.State response = await session.GetZoneState(HomeId, ZoneId); 190 | 191 | Assert.IsInstanceOfType(response.OpenWindowDetected, typeof(bool)); 192 | } 193 | 194 | /// 195 | /// Test to show how IsOpenWindowDetected can be used on a zone's state. 196 | /// 197 | [TestMethod] 198 | public async Task GetZoneTemperatureOffset() 199 | { 200 | Entities.Zone[] zones = await session.GetZones(HomeId); 201 | Entities.Zone zone = zones.FirstOrDefault(z => z.Id == ZoneId); 202 | Entities.Temperature response = await session.GetZoneTemperatureOffset(zone.Devices[0]); 203 | 204 | Assert.IsNotNull(response.Celsius); 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tado API 2 | This library compiled for .NET 9 and .NET 10 will allow you to easily communicate with the Tado API and retrieve details about your Tado thermostats and zones and set their temperature. 3 | 4 | [![licence badge]][licence] 5 | [![Continuous Integration Build](https://github.com/KoenZomers/TadoApi/actions/workflows/cibuild.yml/badge.svg)](https://github.com/KoenZomers/UniFiApi/actions/workflows/cibuild.yml) 6 | 7 | [licence badge]:https://img.shields.io/badge/license-Apache2-blue.svg 8 | [licence]:https://github.com/koenzomers/TadoApi/blob/master/LICENSE.md 9 | 10 | ## Version History 11 | 0.7.0.0 - released November 19, 2025 12 | 13 | - Added library for .NET 10 14 | - Implemented optional cancellation token in HTTP calls to be able to cancel (long running) awaitable and retryable calls. 15 | 16 | 0.6.0.0 - released September 17, 2025 17 | 18 | - Added handling for throttling since Tado started to roll out their new throttling. More info [here](https://community.home-assistant.io/t/tado-rate-limiting-api-calls/928751). In the old code it would just return empty lists when throttled, now it will throw a specific `Exceptions.RequestThrottledException` exception which holds all the details about the thottling, such as when it will be reset and what your daily quota is based on your subscription with Tado. 19 | 20 | 0.5.4.0 - released July 10, 2025 21 | 22 | - Fixed an issue with GetZoneState not working 23 | 24 | 0.5.1.0 - released June 26, 2025 25 | 26 | - Upgraded to .NET 9 27 | - Rewrite to use Dependency Injenction 28 | - Applied new Device Auth Flow which is [mandatory by Tado](https://support.tado.com/en/articles/8565472-how-do-i-authenticate-to-access-the-rest-api) as of March 21, 2025. 29 | - It is NOT backwards compatible with previous versions of this library. You will need to rewrite your code to use the new API. 30 | 31 | 0.4.3.0 - released March 31, 2021 32 | 33 | - Added `SetDeviceChildLock` to allow for enabling or disabling the child lock on a Tado device 34 | 35 | 0.4.2.0 - released February 3, 2021 36 | 37 | - Added `GetZoneTemperatureOffset` to allow for getting the current temperature offset in a zone 38 | - Added several variants of `SetZoneTemperatureOffsetFahrenheit` and `SetZoneTemperatureOffsetCelcius` which allow for the zone/room temperature offset to be set 39 | 40 | 0.4.1.0 - released April 19, 2020 41 | 42 | - Added `SetHomePresence` to allow forcing the home state. Only seems to work for Tado v3+ members. 43 | - Added `SetOpenWindow` and `ResetOpenWindow` to force the window state on a zone. 44 | - Added `OpenWindowDetected` to `State` to get information on if an open window has been detected. 45 | Thanks to [AndreasHassing](https://github.com/AndreasHassing) for his contributions to add these features. 46 | 47 | 0.4.0.1 - released January 15, 2020 48 | 49 | - GetMe() wasn't returning any data when using the NuGet package. Fixed that in this version. Thanks to [twiettwiet](https://github.com/twiettwiet) for reporting this through [Issue #3](https://github.com/KoenZomers/TadoApi/issues/3) 50 | 51 | 0.4.0 - released March 4, 2019 52 | 53 | - Converted the .NET Framework API library to .NET Standard 2.0 so it can be used from non Windows environments as well 54 | - Updated Newtonsoft.JSON to version 12.0.1 55 | - Updated the Unit Test packages to the latest version so the Unit Tests work again with the latest version of Visual Studio 2017 56 | 57 | 0.3.1 - released July 17, 2018 58 | 59 | - Marked SetTemperatureCelsius and SetTemperatureFahrenheit as deprecated in favor of using SetHeatingTemperatureCelsius and SetHeatingTemperatureFahrenheit so it becomes clearer that the methods will set a heating temperature. The deprecated methods will stay for a few versions longer for backwards compatibility but will eventually be removed to avoid clutter in the code. 60 | - Added a SetTemperature method which is the basic method used by the other temperature methods and removes duplicate code. As it may be tricky to use this method properly due to the many parameters, it is advised to use one of the methods that are targeting a specific goal, i.e. SetHeatingTemperatureCelcius or SetHotWaterTemperatureCelcius. 61 | - Added comments to more entities to clarify what information they contain 62 | - Added more enums for entities which seem to only be certain values. It may be that this breaks your current code by entities changing type from i.e. string to an enumerator. 63 | - For the Hot Water boiler in Tado I'm currently assuming that this is always zone 0. If this isn't the case for your Tado setup, please let me know and I'll have to change the code to suit this scenario. 64 | 65 | 0.3 - released March 26, 2018 66 | 67 | - Fixed an issue where a recursive loop would arise causing a StackOverflow exception as soon as the access token would expire and the refresh token would be used to get a new access token 68 | - Updated the Client ID and Client Secret to be the special ones set up by Tado for 3rd parties, as by their request 69 | - Fixed a disposal issue when providing proxy configuration to the Tado session instance 70 | 71 | 0.2.1 - released March 13, 2018 72 | 73 | - Added option to provide duration of the heating or off to SetTemperatureCelcius, SetTemperatureFahrenheit and SwitchHeatingOff. This can be: until next manual change, until next scheduled event or for a specific duration. 74 | 75 | 0.2 - released January 31, 2018 76 | 77 | - With big thanks to http://blog.scphillips.com/posts/2017/01/the-tado-api-v2/ added various methods that were listed there but not yet implemented. Full list of available functionalities can be found below. 78 | - Removed old code and some code cleanup 79 | 80 | 0.1 - released January 30, 2018 81 | 82 | - Initial version 83 | 84 | ## System Requirements 85 | 86 | This API is built using Microsoft .NET 9 and is fully asynchronous 87 | 88 | ## Warning 89 | 90 | Tado has not officially released an API yet that developers are free to use. This API has been created by mimicking the traffic to their own web application. This means they can change their Api at any time causing this Api and your code to break without advanced notice. Be sure you understand the consequences of this before using any Tado Api in your own software. 91 | 92 | ## Usage Instructions 93 | 94 | Check out the UnitTest project in this solution for full insight in the possibilities and working code samples. If you want to run the Unit Tests, ensure the appsettings.json file is populated with the proper values valid for your scenario. Remember that it requires you to log on through your browser to Tado for it to get an access token. There is no other way anymore. 95 | 96 | ## Available via NuGet 97 | 98 | You can also pull this API in as a NuGet package by adding "KoenZomers.Tado.Api" or running: 99 | 100 | Install-Package KoenZomers.Tado.Api 101 | 102 | Package statistics: https://www.nuget.org/packages/KoenZomers.Tado.Api 103 | 104 | ## Current functionality 105 | 106 | With this API at its current state you can: 107 | 108 | - Authenticate to the Tado v2 API using the device auth flow 109 | - Validate with each request if the access token is still valid (lifetime is 10 minutes / 599 seconds) and if not, uses the refresh token to get a new access token (lifetime is 30 days) 110 | - Retrieve information about the currently logged on user 111 | - Retrieve information about all configured zones in your house 112 | - Retrieve information about all registered Tado devices in your house 113 | - Retrieve information about all connected mobile devices to your house 114 | - Retrieve information about the Tado installations at your house 115 | - Retrieve the users with access to a house 116 | - Retrieve the details of a house 117 | - Get the state if somebody is home 118 | - Get the capabilities of a zone 119 | - Get the settings of a mobile device with access to a house 120 | - Get the summarized overview of zone 121 | - Get information about the weather around your house 122 | - Get the early start setting of a zone 123 | - Switch the early start setting of a zone to enabled or disabled 124 | - Set the desired heating temperature in a zone in Celsius and Fahrenheit 125 | - Switch off the heating in a zone 126 | - Switch the hot water boiler on and off 127 | - Show Hi on Tado thermostats or Tado knobs 128 | - Get the temperature offset for a zone/room/device 129 | - Set the temperature offset for a zone/room/device 130 | - Enable or disable the child lock feature on a Tado device 131 | 132 | ## Still missing 133 | 134 | - Setting heating schedules 135 | 136 | ## Feedback 137 | 138 | I cannot and will not provide support for this library. You can use it as is. Feel free to fork off to create your own version out of it. Pull Requests and Issues are not accepted. 139 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /UnitTest/DataSetTest.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.VisualStudio.TestTools.UnitTesting; 2 | using System.Linq; 3 | using System.Threading.Tasks; 4 | 5 | namespace KoenZomers.Tado.UnitTest; 6 | 7 | /// 8 | /// Unit Tests for changing settings through the Tado API 9 | /// 10 | [TestClass] 11 | public class DataSetTest : BaseTest 12 | { 13 | /// 14 | /// Tado Session to use for all tests 15 | /// 16 | private static Session session; 17 | 18 | /// 19 | /// Sets up a session to be used by all test methods in this class 20 | /// 21 | [ClassInitialize] 22 | public static async Task ClassInitialize(TestContext testContext) 23 | { 24 | session = new Session(Username, Password); 25 | await session.Authenticate(); 26 | } 27 | 28 | /// 29 | /// Cleans up the session that was used by all test methods in this class 30 | /// 31 | [ClassCleanup] 32 | public static void ClassCleanup() 33 | { 34 | session.Dispose(); 35 | } 36 | 37 | /// 38 | /// Test if the temperature can be set in Celsius 39 | /// 40 | [TestMethod] 41 | public async Task SetHeatingTemperatureCelciusTest() 42 | { 43 | // Get the current settings in the zone so we can set it back again 44 | var zone = await session.GetSummarizedZoneState(HomeId, ZoneId); 45 | 46 | // Test setting the zone heating temperature to another temperature 47 | var response = await session.SetHeatingTemperatureCelsius(HomeId, ZoneId, 5.5); 48 | Assert.IsNotNull(response, "Failed to set the temperature of a zone"); 49 | 50 | if (zone.Setting.Temperature.Celsius.HasValue) 51 | { 52 | // Set the zone heating temperature back to its original temperature 53 | await session.SetHeatingTemperatureCelsius(HomeId, ZoneId, zone.Setting.Temperature.Celsius.Value); 54 | } 55 | } 56 | 57 | /// 58 | /// Test if the temperature can be set in Fahrenheit 59 | /// 60 | [TestMethod] 61 | public async Task SetHeatingTemperatureFahrenheitTest() 62 | { 63 | // Get the current settings in the zone so we can set it back again 64 | var zone = await session.GetSummarizedZoneState(HomeId, ZoneId); 65 | 66 | // Test setting the zone heating temperature to another temperature 67 | var response = await session.SetHeatingTemperatureFahrenheit(HomeId, ZoneId, 42); 68 | Assert.IsNotNull(response, "Failed to set the temperature of a zone"); 69 | 70 | if (zone.Setting.Temperature.Fahrenheit.HasValue) 71 | { 72 | // Set the zone heating temperature back to its original temperature 73 | await session.SetHeatingTemperatureFahrenheit(HomeId, ZoneId, zone.Setting.Temperature.Fahrenheit.Value); 74 | } 75 | } 76 | 77 | /// 78 | /// Test if the heating can be switched off 79 | /// 80 | [TestMethod] 81 | public async Task SwitchHeatingOffTest() 82 | { 83 | // Get the current settings in the zone so we can set it back again 84 | var zone = await session.GetSummarizedZoneState(HomeId, ZoneId); 85 | 86 | Entities.ZoneSummary response; 87 | if(zone.Setting.Power == Enums.PowerStates.On) 88 | { 89 | response = await session.SwitchHeatingOff(HomeId, ZoneId); 90 | } 91 | else 92 | { 93 | response = await session.SetHeatingTemperatureCelsius(HomeId, ZoneId, 10); 94 | } 95 | Assert.IsNotNull(response, "Failed to switch the heating in a zone"); 96 | 97 | // Switch the heating setting back to its initial value 98 | if (zone.Setting.Power == Enums.PowerStates.On) 99 | { 100 | await session.SetHeatingTemperatureCelsius(HomeId, ZoneId, zone.Setting.Temperature.Celsius.Value); 101 | } 102 | else 103 | { 104 | await session.SwitchHeatingOff(HomeId, ZoneId); 105 | } 106 | } 107 | 108 | /// 109 | /// Test if the heating can be switched off 110 | /// 111 | [TestMethod] 112 | public async Task SetEarlyStartTest() 113 | { 114 | // Get the current settings in the zone so we can set it back again 115 | var earlyStart = await session.GetEarlyStart(HomeId, ZoneId); 116 | 117 | // Switch the EarlyStart setting 118 | var response = await session.SetEarlyStart(HomeId, ZoneId, !earlyStart.Enabled); 119 | Assert.IsNotNull(response, "Failed to switch the EarlyStart setting of a zone"); 120 | 121 | // Switch the EarlyStart setting back to its initial value 122 | await session.SetEarlyStart(HomeId, ZoneId, earlyStart.Enabled); 123 | } 124 | 125 | /// 126 | /// Test if showing Hi works on a device 127 | /// 128 | [TestMethod] 129 | public async Task SayHiTest() 130 | { 131 | var success = await session.SayHi(DeviceId); 132 | Assert.IsTrue(success, "Failed to display Hi on a Tado device"); 133 | } 134 | 135 | /// 136 | /// Test if the temperature of the hot water boiler can be set in Fahrenheit 137 | /// 138 | [TestMethod] 139 | public async Task SetHotWaterTemperatureFahrenheitTest() 140 | { 141 | // Get the current settings in the zone so we can set it back again. Assuming that hot water is always zone 0. Have yet to verify this. 142 | var zone = await session.GetSummarizedZoneState(HomeId, 0); 143 | 144 | // Test setting the hot water boiler temperature to another temperature 145 | var response = await session.SetHotWaterTemperatureFahrenheit(HomeId, 115, Enums.DurationModes.UntilNextManualChange); 146 | Assert.IsNotNull(response, "Failed to set the temperature of the hot water boiler"); 147 | 148 | if (zone.Setting.Power == Enums.PowerStates.On) 149 | { 150 | // Set the hot water boiler temperature back to its original temperature 151 | await session.SetHotWaterTemperatureFahrenheit(HomeId, zone.Setting.Temperature.Fahrenheit.Value, zone.Termination.CurrentType.Value); 152 | } 153 | else 154 | { 155 | // Switch the hot water boiler back off again 156 | response = await session.SwitchHotWaterOff(HomeId, zone.Termination.CurrentType.Value); 157 | } 158 | } 159 | 160 | /// 161 | /// Test if the temperature of the hot water boiler can be set in Celcius 162 | /// 163 | [TestMethod] 164 | public async Task SetHotWaterTemperatureCelciusTest() 165 | { 166 | // Get the current settings in the zone so we can set it back again. Assuming that hot water is always zone 0. Have yet to verify this. 167 | var zone = await session.GetSummarizedZoneState(HomeId, 0); 168 | 169 | // Test setting the hot water boiler temperature to another temperature 170 | var response = await session.SetHotWaterTemperatureCelcius(HomeId, 45, Enums.DurationModes.UntilNextManualChange); 171 | Assert.IsNotNull(response, "Failed to set the temperature of the hot water boiler"); 172 | 173 | if (zone.Setting.Power == Enums.PowerStates.On) 174 | { 175 | // Set the hot water boiler temperature back to its original temperature 176 | await session.SetHotWaterTemperatureCelcius(HomeId, zone.Setting.Temperature.Celsius.Value, zone.Termination.CurrentType.Value); 177 | } 178 | else 179 | { 180 | // Switch the hot water boiler back off again 181 | response = await session.SwitchHotWaterOff(HomeId, zone.Termination.CurrentType.Value); 182 | } 183 | } 184 | 185 | /// 186 | /// Test if the hot water boiler can be switched off 187 | /// 188 | [TestMethod] 189 | public async Task SwitchHotWaterOffTest() 190 | { 191 | // Get the current settings in the zone so we can set it back again. Assuming that hot water is always zone 0. Have yet to verify this. 192 | var zone = await session.GetSummarizedZoneState(HomeId, 0); 193 | 194 | Entities.ZoneSummary response; 195 | if (zone.Setting.Power == Enums.PowerStates.On) 196 | { 197 | response = await session.SwitchHotWaterOff(HomeId, Enums.DurationModes.UntilNextManualChange); 198 | } 199 | else 200 | { 201 | response = await session.SetHotWaterTemperatureCelcius(HomeId, 45, Enums.DurationModes.UntilNextManualChange); 202 | } 203 | Assert.IsNotNull(response, "Failed to switch the temperature of the hot water boiler"); 204 | 205 | // Switch the heating setting back to its initial value 206 | if (zone.Setting.Power == Enums.PowerStates.On) 207 | { 208 | await session.SetHotWaterTemperatureCelcius(HomeId, zone.Setting.Temperature.Celsius.Value, zone.Termination.CurrentType.Value); 209 | } 210 | else 211 | { 212 | await session.SwitchHotWaterOff(HomeId, zone.Termination.CurrentType.Value); 213 | } 214 | } 215 | 216 | [TestMethod] 217 | public async Task SwitchHomePresenceAwayThenHomeTest() 218 | { 219 | Assert.IsTrue(await session.SetHomePresence(HomeId, Enums.HomePresence.Away)); 220 | 221 | Assert.IsTrue(await session.SetHomePresence(HomeId, Enums.HomePresence.Home)); 222 | } 223 | 224 | /// 225 | /// Test if a zone temperature offset in Celsius can be set 226 | /// 227 | [TestMethod] 228 | public async Task SetZoneTemperatureOffsetInCelciusTest() 229 | { 230 | // Get the zones 231 | var zones = await session.GetZones(HomeId); 232 | 233 | if (zones == null || zones.Length == 0 || zones[0].Devices == null | zones[0].Devices.Length == 0) 234 | { 235 | Assert.Inconclusive("Test inconclusive as the test data is not valid"); 236 | } 237 | 238 | var zone = zones.FirstOrDefault(z => z.Id == ZoneId); 239 | 240 | if(zone == null) 241 | { 242 | Assert.Inconclusive($"Failed to retrive zone with Id {ZoneId}"); 243 | } 244 | 245 | // Get the currently set offset 246 | var currentOffset = await session.GetZoneTemperatureOffset(zone.Devices[0]); 247 | 248 | // Test setting the offset of the first zone 249 | var response = await session.SetZoneTemperatureOffsetCelcius(zone.Devices[0], 1); 250 | Assert.IsNotNull(response, "Failed to set the offset temperature of a zone"); 251 | 252 | if (currentOffset != null) 253 | { 254 | // Set the zone temperature offset back to its original offset 255 | await session.SetZoneTemperatureOffsetCelcius(zone.Devices[0], currentOffset.Celsius.Value); 256 | } 257 | } 258 | 259 | /// 260 | /// Test if a zone temperature offset in Fahrenheit can be set 261 | /// 262 | [TestMethod] 263 | public async Task SetZoneTemperatureOffsetInFahrenheit() 264 | { 265 | // Get the zones 266 | var zones = await session.GetZones(HomeId); 267 | 268 | if (zones == null || zones.Length == 0 || zones[0].Devices == null | zones[0].Devices.Length == 0) 269 | { 270 | Assert.Inconclusive("Test inconclusive as the test data is not valid"); 271 | } 272 | 273 | var zone = zones.FirstOrDefault(z => z.Id == ZoneId); 274 | 275 | if (zone == null) 276 | { 277 | Assert.Inconclusive($"Failed to retrive zone with Id {ZoneId}"); 278 | } 279 | 280 | // Get the currently set offset 281 | var currentOffset = await session.GetZoneTemperatureOffset(zone.Devices[0]); 282 | 283 | // Test setting the offset of the first zone 284 | var response = await session.SetZoneTemperatureOffsetFahrenheit(zone.Devices[0], 1.8); 285 | Assert.IsNotNull(response, "Failed to set the offset temperature of a zone"); 286 | 287 | if (currentOffset != null) 288 | { 289 | // Set the zone temperature offset back to its original offset 290 | await session.SetZoneTemperatureOffsetFahrenheit(zone.Devices[0], currentOffset.Fahrenheit.Value); 291 | } 292 | } 293 | 294 | /// 295 | /// Test if the child lock can be set 296 | /// 297 | [TestMethod] 298 | public async Task SetChildLockEnabled() 299 | { 300 | var devices = await session.GetDevices(HomeId); 301 | 302 | if (devices == null || devices.Length == 0) 303 | { 304 | Assert.Inconclusive("Test inconclusive as the test data is not valid"); 305 | } 306 | 307 | var device = devices.FirstOrDefault(d => d.ShortSerialNo == DeviceId); 308 | 309 | if (device == null) 310 | { 311 | Assert.Inconclusive("Test inconclusive as the test data is not valid"); 312 | } 313 | 314 | if (!device.ChildLockEnabled.HasValue) 315 | { 316 | Assert.Inconclusive("Test inconclusive as the test device does not have childlock functionality"); 317 | } 318 | 319 | var currentChildLockState = device.ChildLockEnabled.Value; 320 | 321 | var response1 = await session.SetDeviceChildLock(device, !currentChildLockState); 322 | Assert.IsTrue(response1, "Failed to flip the child lock setting"); 323 | 324 | var response2 = await session.SetDeviceChildLock(device, currentChildLockState); 325 | Assert.IsTrue(response2, "Failed to return the child lock to its initial value"); 326 | } 327 | } 328 | -------------------------------------------------------------------------------- /Api/Controllers/Http.cs: -------------------------------------------------------------------------------- 1 | using KoenZomers.Tado.Api.Helpers; 2 | using Microsoft.Extensions.Logging; 3 | using Microsoft.Extensions.Options; 4 | using System.Net; 5 | using System.Text; 6 | using System.Text.Json; 7 | 8 | namespace KoenZomers.Tado.Api.Controllers; 9 | 10 | /// 11 | /// Controller wich allows to perform HTTP calls 12 | /// 13 | public class Http : Base 14 | { 15 | #region Properties 16 | 17 | /// 18 | /// Default timeout for HTTP requests in seconds 19 | /// 20 | private short DefaultApiTimeoutSeconds => _configuration?.CurrentValue.DefaultApiTimeoutSeconds ?? 30; 21 | 22 | #endregion 23 | 24 | #region Fields 25 | 26 | /// 27 | /// HttpClient to use for network communications towards the Tado API 28 | /// 29 | private readonly HttpClient? _tadoHttpClient; 30 | 31 | /// 32 | /// Access to the application configuration 33 | /// 34 | private readonly IOptionsMonitor? _configuration; 35 | 36 | #endregion 37 | 38 | /// 39 | /// Instantiate the HTTP controller 40 | /// 41 | /// HttpClientFactory to use to retrieve a HttpClient from 42 | /// The application configuration 43 | /// LoggerFactory to use to retrieve Logger instance from 44 | public Http(IHttpClientFactory httpClientFactory, IOptionsMonitor configuration, ILoggerFactory loggerFactory) : base(loggerFactory: loggerFactory) 45 | { 46 | _configuration = configuration; 47 | _tadoHttpClient = httpClientFactory.CreateClient("Tado"); 48 | _tadoHttpClient.Timeout = TimeSpan.FromSeconds(DefaultApiTimeoutSeconds); 49 | } 50 | 51 | /// 52 | /// Sends a HTTP POST to the provided uri 53 | /// 54 | /// The querystring parameters to send in the POST body 55 | /// Type of object to try to parse the response JSON into 56 | /// Uri of the webservice to send the message to 57 | /// Cancellation token will be used to cancel the request and exit the retry loop. 58 | /// If provided, in case of a non 2xx response, it will keep retrying the call. Optional, if not provided, it will not retry and throw a Exception if it fails. 59 | /// If provided, in case of a non 2xx response, it will retry the call at most the amount configured through this parameter. Optional, if not provided, it will endlessly retry. If has not been set, this is being ignored. 60 | /// Optional token. If provided, it will be used to authenticate the request. If omitted, it will send the request anonymously. 61 | /// Object of type T with the parsed response 62 | /// Thrown when the request is getting throttled 63 | /// Thrown when the request has failed. 64 | public async Task PostMessageGetResponse(Uri uri, QueryStringBuilder queryBuilder, CancellationToken cancellationToken, short? retryIntervalIfFailed = null, short? maximumRetries = null, Models.Authentication.Token? token = null) 65 | { 66 | ArgumentNullException.ThrowIfNull(uri); 67 | ArgumentNullException.ThrowIfNull(_tadoHttpClient); 68 | 69 | var retryCount = 0; 70 | do 71 | { 72 | if (cancellationToken.IsCancellationRequested) 73 | { 74 | Logger.LogDebug("Cancellation requested, stopping retries for POST to {Uri}", uri); 75 | return default; 76 | } 77 | 78 | // Request the response from the webservice 79 | Logger.LogDebug("Calling Tado API at {Uri} with content: {Content}", uri, queryBuilder.ToString()); 80 | 81 | // Prepare the content to POST 82 | using var content = new StringContent(queryBuilder.ToString(), Encoding.UTF8, "application/x-www-form-urlencoded"); 83 | 84 | // Construct the message towards the webservice 85 | using var request = new HttpRequestMessage(HttpMethod.Post, uri); 86 | 87 | // Check if we should include an Authorization Bearer token 88 | if (token is not null) 89 | { 90 | request.Headers.Authorization = new("Bearer", token.AccessToken); 91 | } 92 | 93 | // Set the content to send along in the message body with the request 94 | request.Content = content; 95 | 96 | retryCount++; 97 | HttpResponseMessage response; 98 | Stream responseContentStream; 99 | try 100 | { 101 | response = await _tadoHttpClient.SendAsync(request, cancellationToken); 102 | responseContentStream = await response.Content.ReadAsStreamAsync(cancellationToken); 103 | } 104 | catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) 105 | { 106 | Logger.LogDebug("{Method} for {Uri} was cancelled", nameof(PostMessageGetResponse), uri); 107 | return default; 108 | } 109 | catch (Exception ex) 110 | { 111 | throw new Exceptions.RequestFailedException(uri, ex); 112 | } 113 | 114 | if (response.StatusCode == HttpStatusCode.TooManyRequests) 115 | { 116 | // Request was throttled 117 | throw new Exceptions.RequestThrottledException(uri, response); 118 | } 119 | if (!response.IsSuccessStatusCode) 120 | { 121 | // Request was not successful 122 | if (!retryIntervalIfFailed.HasValue || retryCount >= maximumRetries) 123 | { 124 | // We should not retry or we have reached the maximum number of retries 125 | throw new Exceptions.RequestFailedException(uri); 126 | } 127 | 128 | // Pause and retry 129 | Logger.LogDebug($"Request failed with status code {response.StatusCode} for URI {uri}. Retrying in {retryIntervalIfFailed.Value} seconds..."); 130 | await Task.Delay(TimeSpan.FromSeconds(retryIntervalIfFailed.Value), cancellationToken); 131 | } 132 | else 133 | { 134 | // Request was successful (response status 200-299) 135 | var responseEntity = await JsonSerializer.DeserializeAsync(responseContentStream, cancellationToken: CancellationToken.None); 136 | return responseEntity; 137 | } 138 | } while (true); 139 | } 140 | 141 | /// 142 | /// Sends a message to the Tado API and returns the provided object of type T with the response 143 | /// 144 | /// Object type of the expected response 145 | /// Uri of the webservice to send the message to 146 | /// Cancellation token will be used to cancel the request. 147 | /// The expected Http result status code. Optional. If provided and the webservice returns a different response, the return type will be NULL to indicate failure. 148 | /// Optional token. If provided, it will be used to authenticate the request. If omitted, it will send the request anonymously. 149 | /// Typed entity with the result from the webservice 150 | /// Thrown when the request is getting throttled 151 | /// Thrown when the request has failed. 152 | public async Task GetMessageReturnResponse(Uri uri, CancellationToken cancellationToken, HttpStatusCode? expectedHttpStatusCode = null, Models.Authentication.Token? token = null) 153 | { 154 | ArgumentNullException.ThrowIfNull(uri); 155 | ArgumentNullException.ThrowIfNull(_tadoHttpClient); 156 | 157 | // Construct the request towards the webservice 158 | using var request = new HttpRequestMessage(HttpMethod.Get, uri); 159 | 160 | // Check if we should include an Authorization Bearer token 161 | if (token is not null) 162 | { 163 | request.Headers.Authorization = new("Bearer", token.AccessToken); 164 | } 165 | 166 | HttpResponseMessage response; 167 | try 168 | { 169 | // Request the response from the webservice 170 | response = await _tadoHttpClient.SendAsync(request, cancellationToken); 171 | } 172 | catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) 173 | { 174 | Logger.LogDebug("{Method} for {Uri} was cancelled", nameof(GetMessageReturnResponse), uri); 175 | return default; 176 | } 177 | catch (Exception ex) 178 | { 179 | // Request was not successful, throw an exception 180 | throw new Exceptions.RequestFailedException(uri, ex); 181 | } 182 | 183 | if (response.StatusCode == HttpStatusCode.TooManyRequests) 184 | { 185 | // Request was throttled 186 | throw new Exceptions.RequestThrottledException(uri, response); 187 | } 188 | if (!expectedHttpStatusCode.HasValue || response.StatusCode == expectedHttpStatusCode.Value) 189 | { 190 | var responseContentStream = await response.Content.ReadAsStreamAsync(CancellationToken.None); 191 | var responseEntity = await JsonSerializer.DeserializeAsync(responseContentStream, cancellationToken: CancellationToken.None); 192 | return responseEntity; 193 | } 194 | return default; 195 | } 196 | 197 | /// 198 | /// Sends a message to the Tado API and returns the provided object of type T with the response 199 | /// 200 | /// Object type of the expected response 201 | /// Uri of the webservice to send the message to 202 | /// Text to send to the webservice in the body 203 | /// Http Method to use to connect to the webservice 204 | /// Cancellation token to cancel the request 205 | /// The expected Http result status code. Optional. If provided and the webservice returns a different response, the return type will be NULL to indicate failure. 206 | /// Optional token. If provided, it will be used to authenticate the request. If omitted, it will send the request anonymously. 207 | /// Typed entity with the result from the webservice 208 | /// Thrown when the request is getting throttled 209 | /// Thrown when the request has failed. 210 | public async Task SendMessageReturnResponse(string bodyText, HttpMethod httpMethod, Uri uri, CancellationToken cancellationToken, HttpStatusCode? expectedHttpStatusCode = null, Models.Authentication.Token? token = null) 211 | { 212 | ArgumentNullException.ThrowIfNull(uri); 213 | ArgumentNullException.ThrowIfNull(_tadoHttpClient); 214 | 215 | // Load the content to send in the body 216 | using var content = new StringContent(bodyText ?? "", Encoding.UTF8, "application/json"); 217 | 218 | // Construct the message towards the webservice 219 | using var request = new HttpRequestMessage(httpMethod, uri); 220 | 221 | // Check if we should include an Authorization Bearer token 222 | if (token is not null) 223 | { 224 | request.Headers.Authorization = new("Bearer", token.AccessToken); 225 | } 226 | 227 | // Check if a body to send along with the request has been provided 228 | if (!string.IsNullOrEmpty(bodyText) && httpMethod != HttpMethod.Get) 229 | { 230 | // Set the content to send along in the message body with the request 231 | request.Content = content; 232 | } 233 | 234 | HttpResponseMessage response; 235 | try 236 | { 237 | // Request the response from the webservice 238 | response = await _tadoHttpClient.SendAsync(request, cancellationToken); 239 | 240 | } 241 | catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) 242 | { 243 | Logger.LogDebug("{Method} for {Uri} was cancelled", nameof(SendMessageReturnResponse), uri); 244 | return default; 245 | } 246 | catch (Exception ex) 247 | { 248 | // Request was not successful. throw an exception 249 | throw new Exceptions.RequestFailedException(uri, ex); 250 | } 251 | 252 | if (response.StatusCode == HttpStatusCode.TooManyRequests) 253 | { 254 | // Request was throttled 255 | throw new Exceptions.RequestThrottledException(uri, response); 256 | } 257 | if (!expectedHttpStatusCode.HasValue || response.StatusCode == expectedHttpStatusCode.Value) 258 | { 259 | var responseContentStream = await response.Content.ReadAsStreamAsync(CancellationToken.None); 260 | var responseEntity = await JsonSerializer.DeserializeAsync(responseContentStream, cancellationToken: CancellationToken.None); 261 | return responseEntity; 262 | } 263 | return default; 264 | } 265 | 266 | /// 267 | /// Sends a message to the Tado API without looking at the response 268 | /// 269 | /// Uri of the webservice to send the message to 270 | /// Text to send to the webservice in the body 271 | /// Http Method to use to connect to the webservice 272 | /// Cancellation token to cancel the request 273 | /// The expected Http result status code. Optional. If provided and the webservice returns a different response, the return type will be false to indicate failure. 274 | /// Optional token. If provided, it will be used to authenticate the request. If omitted, it will send the request anonymously. 275 | /// Boolean indicating if the request was successful 276 | /// Thrown when the request is getting throttled 277 | /// Thrown when the request has failed. 278 | public async Task SendMessage(string bodyText, HttpMethod httpMethod, Uri uri, CancellationToken cancellationToken, HttpStatusCode? expectedHttpStatusCode = null, Models.Authentication.Token? token = null) 279 | { 280 | ArgumentNullException.ThrowIfNull(uri); 281 | ArgumentNullException.ThrowIfNull(_tadoHttpClient); 282 | 283 | // Load the content to send in the body 284 | using var content = new StringContent(bodyText ?? "", Encoding.UTF8, "application/json"); 285 | // Construct the message towards the webservice 286 | using var request = new HttpRequestMessage(httpMethod, uri); 287 | 288 | // Check if we should include an Authorization Bearer token 289 | if (token is not null) 290 | { 291 | request.Headers.Authorization = new("Bearer", token.AccessToken); 292 | } 293 | 294 | // Check if a body to send along with the request has been provided 295 | if (!string.IsNullOrEmpty(bodyText) && httpMethod != HttpMethod.Get) 296 | { 297 | // Set the content to send along in the message body with the request 298 | request.Content = content; 299 | } 300 | 301 | HttpResponseMessage response; 302 | try 303 | { 304 | // Request the response from the webservice 305 | response = await _tadoHttpClient.SendAsync(request, cancellationToken); 306 | } 307 | catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) 308 | { 309 | Logger.LogDebug("{Method} for {Uri} was cancelled", nameof(SendMessage), uri); 310 | return false; 311 | } 312 | catch (Exception ex) 313 | { 314 | // Request was not successful. throw an exception 315 | throw new Exceptions.RequestFailedException(uri, ex); 316 | } 317 | 318 | if (response.StatusCode == HttpStatusCode.TooManyRequests) 319 | { 320 | // Request was throttled 321 | throw new Exceptions.RequestThrottledException(uri, response); 322 | } 323 | if (!expectedHttpStatusCode.HasValue || response.StatusCode == expectedHttpStatusCode.Value) 324 | { 325 | return true; 326 | } 327 | return false; 328 | } 329 | } 330 | -------------------------------------------------------------------------------- /Api/KoenZomers.Tado.Api.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | KoenZomers.Tado.Api 5 | 6 | 7 | 8 | 9 | Converts the Tado device type returned by the Tado API to the DeviceTypes enumerator in this project 10 | 11 | 12 | 13 | 14 | Converts the duration mode type returned by the Tado API to the DurationModes enumerator in this project 15 | 16 | 17 | 18 | 19 | Converts the power state returned by the Tado API to the PowerStates enumerator in this project 20 | 21 | 22 | 23 | 24 | Characteristics of a device 25 | 26 | 27 | 28 | 29 | State of the connection towards a Tado device 30 | 31 | 32 | 33 | 34 | Contains contact details of an owner of a house 35 | 36 | 37 | 38 | 39 | Details about the current configuration of the dazzle mode (animation when changing the temperature) 40 | 41 | 42 | 43 | 44 | Information about one Tado device 45 | 46 | 47 | 48 | 49 | Contains information about a home where Tado is being used 50 | 51 | 52 | 53 | 54 | Information about the state of the home 55 | 56 | 57 | 58 | 59 | Contains detailed information about a house 60 | 61 | 62 | 63 | 64 | Humidity measured by a Tado device 65 | 66 | 67 | 68 | 69 | One Tado installation 70 | 71 | 72 | 73 | 74 | Contains the coordinates relative to the home location where Tado is being used 75 | 76 | 77 | 78 | 79 | Contains detailed information about a device connected to Tado 80 | 81 | 82 | 83 | 84 | Contains information about a mobile device set up to be used with Tado 85 | 86 | 87 | 88 | 89 | Contains the location of a device 90 | 91 | 92 | 93 | 94 | Contains settings specific to the device 95 | 96 | 97 | 98 | 99 | State of the mounted Tado device 100 | 101 | 102 | 103 | 104 | Open Window Detection settings 105 | 106 | 107 | 108 | 109 | The current state of the Tado device 110 | 111 | 112 | 113 | 114 | Information on when the current state of the Tado device will end 115 | 116 | 117 | 118 | 119 | Temperature and humidity measured by a Tado device 120 | 121 | 122 | 123 | 124 | Session token coming forward from an OAuth authentication request 125 | 126 | 127 | 128 | 129 | Date and time at which the Access Token expires 130 | 131 | 132 | 133 | 134 | The current state of a Tado device 135 | 136 | 137 | 138 | 139 | Type of Tado device 140 | 141 | 142 | 143 | 144 | The powerstate of the Tado device 145 | 146 | 147 | 148 | 149 | The temperature the Tado device is set to change the zone to 150 | 151 | 152 | 153 | 154 | State of a specific zone 155 | 156 | 157 | 158 | 159 | Information regarding a temperature 160 | 161 | 162 | 163 | 164 | The temperature in degrees Celcius 165 | 166 | 167 | 168 | 169 | The temperature in degrees Fahrenheit 170 | 171 | 172 | 173 | 174 | Information about when the current state of the Tado device is expected to change 175 | 176 | 177 | 178 | 179 | Defines if and what will make the Tado device change its state 180 | 181 | 182 | 183 | 184 | Date and time at which the termination mode is expected to change. NULL if CurrentType is Manual thus impossible to predict when the next state change will be. 185 | 186 | 187 | 188 | 189 | Date and time at which the termination mode will change. NULL if CurrentType is Manual thus impossible to predict when the next state change will be. 190 | 191 | 192 | 193 | 194 | Amount of seconds remaining before the Tado device will change its state. Will only contain a value if CurrentType is Timer. 195 | 196 | 197 | 198 | 199 | Contains information about a user 200 | 201 | 202 | 203 | 204 | The current weather 205 | 206 | 207 | 208 | 209 | Information about one zone 210 | 211 | 212 | 213 | 214 | Summarized state of a zone 215 | 216 | 217 | 218 | 219 | The current state of the Tado device 220 | 221 | 222 | 223 | 224 | Information on when the current state of the Tado device will end 225 | 226 | 227 | 228 | 229 | Defines the types of Tado devices that can be switched 230 | 231 | 232 | 233 | 234 | Heating 235 | 236 | 237 | 238 | 239 | Hot water 240 | 241 | 242 | 243 | 244 | Defines the modes of the duration of changing a temperature 245 | 246 | 247 | 248 | 249 | Keep the setting until the next scheduled event starts 250 | 251 | 252 | 253 | 254 | Keep the setting for a specific duration 255 | 256 | 257 | 258 | 259 | Keep the setting until the user makes another change 260 | 261 | 262 | 263 | 264 | Defines the power state a Tado device can be in 265 | 266 | 267 | 268 | 269 | Device is ON 270 | 271 | 272 | 273 | 274 | Device is OFF 275 | 276 | 277 | 278 | 279 | Defines the types to which a Tado device can be set that define the end of the current state of the device 280 | 281 | 282 | 283 | 284 | The state will not change unless the device gets explicit instructions to do so 285 | 286 | 287 | 288 | 289 | The state of the device will change at the next scheduled event for the device 290 | 291 | 292 | 293 | 294 | The state of the device will change after a preset amount of time has elapsed 295 | 296 | 297 | 298 | 299 | Exception thrown when a request failed 300 | 301 | 302 | 303 | 304 | Uri that was called 305 | 306 | 307 | 308 | 309 | Exception thrown when authenticating failed 310 | 311 | 312 | 313 | 314 | Exception thrown when functionality is called which requires the session to be authenticated while it isn't yet 315 | 316 | 317 | 318 | 319 | Username to use to connect to the Tado API. Set by providing it in the constructor. 320 | 321 | 322 | 323 | 324 | Password to use to connect to the Tado API. Set by providing it in the constructor. 325 | 326 | 327 | 328 | 329 | Base Uri with which all Tado API requests start 330 | 331 | 332 | 333 | 334 | Tado API Uri to authenticate against 335 | 336 | 337 | 338 | 339 | Tado API Client Id to use for the OAuth token 340 | 341 | 342 | 343 | 344 | Tado API Client Secret to use for the OAuth token 345 | 346 | 347 | 348 | 349 | Allows setting an User Agent which will be provided to the Tado API 350 | 351 | 352 | 353 | 354 | If provided, this proxy will be used for communication with the Tado API. If not provided, no proxy will be used. 355 | 356 | 357 | 358 | 359 | If provided along with a proxy configuration, these credentials will be used to authenticate to the proxy. If omitted, the default system credentials will be used. 360 | 361 | 362 | 363 | 364 | Boolean indicating if the current session is authenticated 365 | 366 | 367 | 368 | 369 | Authenticated Session that will be used to communicate with the Tado API 370 | 371 | 372 | 373 | 374 | HttpClient to use for network communications towards the Tado API 375 | 376 | 377 | 378 | 379 | Initiates a new session to the Tado API 380 | 381 | 382 | 383 | 384 | Clean up 385 | 386 | 387 | 388 | 389 | Validates if the session has authenticated already and if so, ensures the AccessToken coming forward from the authentication is still valid and assigns it to the HttpClient in this session 390 | 391 | 392 | 393 | 394 | Ensures the current session is authenticated or throws an exception if it's not 395 | 396 | 397 | 398 | 399 | Sets up a new session with the Tado API 400 | 401 | Session instance 402 | 403 | 404 | 405 | Sets up a session with the Tado API based on the refresh token 406 | 407 | Session instance 408 | 409 | 410 | 411 | Authenticates this session with the Tado API 412 | 413 | 414 | 415 | 416 | Instantiates a new HttpClient preconfigured for use. Note that the caller is responsible for disposing this object. 417 | 418 | HttpClient instance 419 | 420 | 421 | 422 | Sends a HTTP POST to the provided uri 423 | 424 | The querystring parameters to send in the POST body 425 | Type of object to try to parse the response JSON into 426 | Uri of the webservice to send the message to 427 | True to indicate that this request must have a valid oAuth token 428 | Object of type T with the parsed response 429 | 430 | 431 | 432 | Sends a message to the Tado API and returns the provided object of type T with the response 433 | 434 | Object type of the expected response 435 | Uri of the webservice to send the message to 436 | The expected Http result status code. Optional. If provided and the webservice returns a different response, the return type will be NULL to indicate failure. 437 | Typed entity with the result from the webservice 438 | 439 | 440 | 441 | Sends a message to the Tado API and returns the provided object of type T with the response 442 | 443 | Object type of the expected response 444 | Uri of the webservice to send the message to 445 | Text to send to the webservice in the body 446 | Http Method to use to connect to the webservice 447 | The expected Http result status code. Optional. If provided and the webservice returns a different response, the return type will be NULL to indicate failure. 448 | Typed entity with the result from the webservice 449 | 450 | 451 | 452 | Sends a message to the Tado API without looking at the response 453 | 454 | Uri of the webservice to send the message to 455 | Text to send to the webservice in the body 456 | Http Method to use to connect to the webservice 457 | The expected Http result status code. Optional. If provided and the webservice returns a different response, the return type will be false to indicate failure. 458 | Boolean indicating if the request was successful 459 | 460 | 461 | 462 | Returns information about the user currently connected through the Tado API 463 | 464 | Information about the current user 465 | 466 | 467 | 468 | Returns the zones configured in the home with the provided Id from the Tado API 469 | 470 | Id of the home to query 471 | The configured zones 472 | 473 | 474 | 475 | Returns the devices configured in the home with the provided Id from the Tado API 476 | 477 | Id of the home to query 478 | The configured devices 479 | 480 | 481 | 482 | Returns the mobile devices connected to the home with the provided Id from the Tado API 483 | 484 | Id of the home to query 485 | The connected mobile devices 486 | 487 | 488 | 489 | Returns the settings of a mobile device connected to the home with the provided Id from the Tado API 490 | 491 | Id of the home to query 492 | Id of the mobile device to query 493 | The settings of the connected mobile device 494 | 495 | 496 | 497 | Returns the installations in the home with the provided Id from the Tado API 498 | 499 | Id of the home to query 500 | The installations 501 | 502 | 503 | 504 | Returns the state of the home with the provided Id from the Tado API 505 | 506 | Id of the home to query 507 | The state of the home 508 | 509 | 510 | 511 | Returns the state of a zone in the home with the provided Id from the Tado API 512 | 513 | Id of the home to query 514 | Id of the zone to query 515 | The state of the zone 516 | 517 | 518 | 519 | Returns the summarized state of a zone in the home with the provided Id from the Tado API 520 | 521 | Id of the home to query 522 | Id of the zone to query 523 | The summarized state of the zone 524 | 525 | 526 | 527 | Returns the current weather at the home with the provided Id from the Tado API 528 | 529 | Id of the home to query 530 | The current weater at the home 531 | 532 | 533 | 534 | Returns the home with the provided Id from the Tado API 535 | 536 | Id of the home to query 537 | The home details 538 | 539 | 540 | 541 | Returns the users with access to the home with the provided Id from the Tado API 542 | 543 | Id of the home to query 544 | The users with access 545 | 546 | 547 | 548 | Returns the capabilities of a zone in the home with the provided Id from the Tado API 549 | 550 | Id of the home to query 551 | Id of the zone to query 552 | The capabilities of the zone 553 | 554 | 555 | 556 | Returns the early start of a zone in the home with the provided Id from the Tado API 557 | 558 | Id of the home to query 559 | Id of the zone to query 560 | The early start setting of the zone 561 | 562 | 563 | 564 | Sets the temperature in a zone in the home with the provided Id through the Tado API 565 | 566 | Id of the home to set the temperature of 567 | Id of the zone to set the temperature of 568 | Temperature to set the zone to 569 | The summarized new state of the zone 570 | 571 | 572 | 573 | Sets the temperature in a zone in the home with the provided Id through the Tado API 574 | 575 | Id of the home to set the temperature of 576 | Id of the zone to set the temperature of 577 | Temperature to set the zone to 578 | The summarized new state of the zone 579 | 580 | 581 | 582 | Sets the temperature in a zone in the home with the provided Id through the Tado API for the duration as specified 583 | 584 | Id of the home to set the temperature of 585 | Id of the zone to set the temperature of 586 | Temperature to set the zone to 587 | Defines the duration for which the heating will be switched to the provided temperature 588 | Only applicapble if for durationMode Timer has been chosen. In that case it allows providing for how long the duration should be. 589 | The summarized new state of the zone 590 | 591 | 592 | 593 | Sets the temperature in a zone in the home with the provided Id through the Tado API for the duration as specified 594 | 595 | Id of the home to set the temperature of 596 | Id of the zone to set the temperature of 597 | Temperature to set the zone to 598 | Defines the duration for which the heating will be switched to the provided temperature 599 | Only applicapble if for durationMode Timer has been chosen. In that case it allows providing for how long the duration should be. 600 | The summarized new state of the zone 601 | 602 | 603 | 604 | Sets the hot water temperature in the home with the provided Id through the Tado API for the duration as specified 605 | 606 | Id of the home to set the temperature of 607 | Temperature in Celcius to set the zone to 608 | Defines the duration for which the heating will be switched to the provided temperature 609 | Only applicapble if for durationMode Timer has been chosen. In that case it allows providing for how long the duration should be. 610 | The summarized new state of the zone 611 | 612 | 613 | 614 | Sets the hot water temperature in the home with the provided Id through the Tado API for the duration as specified 615 | 616 | Id of the home to set the temperature of 617 | Temperature in Fahrenheit to set the zone to 618 | Defines the duration for which the heating will be switched to the provided temperature 619 | Only applicapble if for durationMode Timer has been chosen. In that case it allows providing for how long the duration should be. 620 | The summarized new state of the zone 621 | 622 | 623 | 624 | Sets the temperature in a zone in the home with the provided Id through the Tado API for the duration as specified 625 | 626 | Id of the home to set the temperature of 627 | Id of the zone to set the temperature of 628 | Temperature in Celcius to set the zone to. Provide NULL for both temperatureCelcius and temperatureFahrenheit to switch the device off. 629 | Temperature in Fahrenheit to set the zone to. Provide NULL for both temperatureCelcius and temperatureFahrenheit to switch the device off. 630 | Defines the duration for which the heating will be switched to the provided temperature 631 | Only applicapble if for durationMode Timer has been chosen. In that case it allows providing for how long the duration should be. 632 | Type of Tado device to switch on 633 | The summarized new state of the zone 634 | 635 | 636 | 637 | Sets the temperature in a zone in the home with the provided Id through the Tado API 638 | 639 | Id of the home to set the temperature of 640 | Id of the zone to set the temperature of 641 | Temperature to set the zone to 642 | The summarized new state of the zone 643 | 644 | 645 | 646 | Sets the temperature in a zone in the home with the provided Id through the Tado API 647 | 648 | Id of the home to set the temperature of 649 | Id of the zone to set the temperature of 650 | Temperature to set the zone to 651 | The summarized new state of the zone 652 | 653 | 654 | 655 | Sets the temperature in a zone in the home with the provided Id through the Tado API for the duration as specified 656 | 657 | Id of the home to set the temperature of 658 | Id of the zone to set the temperature of 659 | Temperature to set the zone to 660 | Defines the duration for which the heating will be switched to the provided temperature 661 | Only applicapble if for durationMode Timer has been chosen. In that case it allows providing for how long the duration should be. 662 | The summarized new state of the zone 663 | 664 | 665 | 666 | Sets the temperature in a zone in the home with the provided Id through the Tado API for the duration as specified 667 | 668 | Id of the home to set the temperature of 669 | Id of the zone to set the temperature of 670 | Temperature to set the zone to 671 | Defines the duration for which the heating will be switched to the provided temperature 672 | Only applicapble if for durationMode Timer has been chosen. In that case it allows providing for how long the duration should be. 673 | The summarized new state of the zone 674 | 675 | 676 | 677 | Switches the heating off in a zone in the home with the provided Id through the Tado API. Use SetTemperatureCelsius or SetTemperatureFahrenheit to switch the heating on again. 678 | 679 | Id of the home to switch the heating off in 680 | Id of the zone to switch the heating off in 681 | The summarized new state of the zone 682 | 683 | 684 | 685 | Switches the heating off in a zone in the home with the provided Id through the Tado API for the duration as specified 686 | 687 | Id of the home to switch the heating off in 688 | Id of the zone to switch the heating off in 689 | Defines the duration for which the temperature will remain switched off 690 | Only applicapble if for durationMode Timer has been chosen. In that case it allows providing for how long the duration should be. 691 | The summarized new state of the zone 692 | 693 | 694 | 695 | Switches the hot water off in the home with the provided Id through the Tado API for the duration as specified 696 | 697 | Id of the home to switch the heating off in 698 | Defines the duration for which the temperature will remain switched off 699 | Only applicapble if for durationMode Timer has been chosen. In that case it allows providing for how long the duration should be. 700 | The summarized new state of the zone 701 | 702 | 703 | 704 | Sets the EarlyStart mode of a zone in the home with the provided Id through the Tado API 705 | 706 | Id of the home to switch the heating off in 707 | Id of the zone to switch the heating off in 708 | True to enable EarlyStart or False to disable it 709 | The new EarlyStart mode of the zone 710 | 711 | 712 | 713 | Shows Hi on the Tado device to identify it 714 | 715 | Id / serial number of the Tado device 716 | Boolean indicating if the request was successful 717 | 718 | 719 | 720 | --------------------------------------------------------------------------------