├── 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 | [](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 |
--------------------------------------------------------------------------------