[tado] Add rate limit info channels (#19766)

* First commit
Adds rate limit info channels

Signed-off-by: Erik De Boeck <deboeck.erik@gmail.com>
This commit is contained in:
ErikDB87
2025-12-28 18:46:37 +01:00
committed by GitHub
parent 7fb5b5115a
commit 08d76d18d5
7 changed files with 186 additions and 34 deletions
+33 -29
View File
@@ -15,7 +15,7 @@ It must be authenticated before it will actually go online.
Authenticatation is done online via the OAuth Device Code Grant Flow (RFC-8628) authentication process via the link provided at `http://[openhab-ip-address]:8080/tado`.
| Parameter | Optional | Description | Default |
|---------------|----------|-------------------------------------------------------------------------------|------------------------------|
| ------------- | -------- | ----------------------------------------------------------------------------- | ---------------------------- |
| `rfcWithUser` | yes | Determines if the user name is included in the oAuth RFC-8628 authentication. | |
| `tadoApiUrl` | no | Selects the URL to be used for API calls (it's possible to use a proxy) | `https://my.tado.com/api/v2` |
| `username` | yes | Selects the tado° account to be used if there is more than one account. | |
@@ -42,10 +42,14 @@ Once the `home` thing is online, the binding will discover all its respective zo
### Channels
| Name | Type | Description | Read/Write |
|---------------------|--------|---------------------------------------------------------------------|------------|
| `homePresence` | Switch | Current presence value of the tado home; `ON` = HOME / `OFF` = AWAY | RW |
| `geofencingEnabled` | Switch | Selects if automatic geofencing is enabled or disabled | RW |
| Name | Type | Description | Read/Write |
| ------------------- | ----------- | -------------------------------------------------------------------------- | ---------- |
| `homePresence` | Switch | Current presence value of the tado home; `ON` = HOME / `OFF` = AWAY | RW |
| `geofencingEnabled` | Switch | Selects if automatic geofencing is enabled or disabled | RW |
| `apiRateRemaining` | Number | Number of API calls remaining before the maximum is reached | R |
| `apiRateLimit` | Number | Maximum API calls allowed per specified duration | R |
| `apiRateDuration` | Number:Time | Duration in which the API count rises until maximum | R |
| `apiRateReset` | Number:Time | Duration before the API count resets (only when the API limit is breached) | R |
## `zone` Thing
@@ -54,7 +58,7 @@ You have defined them during installation.
One zone relates to one page in the tado° mobile- or webapp.
| Parameter | Required | Description | Default |
|----------------------|----------|---------------------------------------------------------------|---------|
| -------------------- | -------- | ------------------------------------------------------------- | ------- |
| `id` | yes | Zone Id | - |
| `refreshInterval` | no | Refresh interval of state updates in seconds | 30 |
| `hvacChangeDebounce` | no | Duration in seconds to combine multiple HVAC changes into one | 5 |
@@ -79,26 +83,26 @@ A zone is either of type `HEATING`, `AC` or `DHW` (domestic hot water).
The availability of items as well as their allowed values depend on type and capabilities of the HVAC setup.
If you are unsure, have a look at the tado° app and see if the functionality is available and what values are supported.
| Name | Type | Description | Read/Write | Zone type |
|--------------------------------|----------------------|-------------------------------------------------------------------------------------------------------------------|------------|------------------------|
| `currentTemperature` | Number:Temperature | Current inside temperature | R | `HEATING`, `AC` |
| `humidity` | Number:Dimensionless | Current relative inside humidity in percent | R | `HEATING`, `AC` |
| `hvacMode` | String | Active mode, one of `OFF`, `HEAT`, `COOL`, `DRY`, `FAN`, `AUTO` | RW | `HEATING` and `DHW` support `OFF` and `HEAT`, `AC` can support more |
| `targetTemperature` | Number:Temperature | Set point | RW | `HEATING`, `AC`, `DHW` |
| `operationMode` | String | Operation mode the zone is currently in. One of `SCHEDULE` (follow smart schedule), `MANUAL` (override until ended manually), `TIMER` (override for a given time), `UNTIL_CHANGE` (active until next smart schedule block or until AWAY mode becomes active) | RW | `HEATING`, `AC`, `DHW` |
| `overlayExpiry` | DateTime | End date and time of a timer | R | `HEATING`, `AC`, `DHW` |
| `timerDuration` | Number | Timer duration in minutes | RW | `HEATING`, `AC`, `DHW` |
| `heatingPower` | Number:Dimensionless | Amount of heating power currently present | R | `HEATING` |
| `acPower` | Switch | Indicates if the Air-Conditioning is Off or On | R | `AC` |
| `fanspeed`<sup>1)</sup> | String | Fan speed, one of `AUTO`, `LOW`, `MIDDLE`, `HIGH` | RW | `AC` |
| `fanLevel`<sup>1)</sup> | String | Fan speed, one of <sup>3)</sup> `AUTO`, `SILENT`, `LEVEL1`, `LEVEL2`, `LEVEL3`, `LEVEL4`, `LEVEL5` | RW | `AC` |
| `swing`<sup>2)</sup> | Switch | Swing on/off | RW | `AC` |
| `verticalSwing`<sup>2)</sup> | String | Vertical swing state, one of <sup>3)</sup> `OFF`, `ON`, `UP`, `MID_UP`, `MID`, `MID_DOWN`, `DOWN`, `AUTO` | RW | `AC` |
| `horizontalSwing`<sup>2)</sup> | String | Horizontal swing state, one of <sup>3)</sup> `OFF`, `ON`, `LEFT`, `MID_LEFT`, `MID`, `MID_RIGHT`, `RIGHT`, `AUTO` | RW | `AC` |
| `batteryLowAlarm` | Switch | A control device in the Zone has a low battery | R | Any Zone |
| `openWindowDetected` | Switch | An open window has been detected in the Zone | R | `HEATING`, `AC` |
| `openWindowRemainingTime` | Number:Time | The remaining Open Window heating/cooling Override time in the Zone | R | `HEATING`, `AC` |
| `light` | Switch | State (`ON`, `OFF`) of the control panel light | RW | `AC` |
| Name | Type | Description | Read/Write | Zone type |
| ------------------------------ | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------- | ------------------------------------------------------------------- |
| `currentTemperature` | Number:Temperature | Current inside temperature | R | `HEATING`, `AC` |
| `humidity` | Number:Dimensionless | Current relative inside humidity in percent | R | `HEATING`, `AC` |
| `hvacMode` | String | Active mode, one of `OFF`, `HEAT`, `COOL`, `DRY`, `FAN`, `AUTO` | RW | `HEATING` and `DHW` support `OFF` and `HEAT`, `AC` can support more |
| `targetTemperature` | Number:Temperature | Set point | RW | `HEATING`, `AC`, `DHW` |
| `operationMode` | String | Operation mode the zone is currently in. One of `SCHEDULE` (follow smart schedule), `MANUAL` (override until ended manually), `TIMER` (override for a given time), `UNTIL_CHANGE` (active until next smart schedule block or until AWAY mode becomes active) | RW | `HEATING`, `AC`, `DHW` |
| `overlayExpiry` | DateTime | End date and time of a timer | R | `HEATING`, `AC`, `DHW` |
| `timerDuration` | Number | Timer duration in minutes | RW | `HEATING`, `AC`, `DHW` |
| `heatingPower` | Number:Dimensionless | Amount of heating power currently present | R | `HEATING` |
| `acPower` | Switch | Indicates if the Air-Conditioning is Off or On | R | `AC` |
| `fanspeed`<sup>1)</sup> | String | Fan speed, one of `AUTO`, `LOW`, `MIDDLE`, `HIGH` | RW | `AC` |
| `fanLevel`<sup>1)</sup> | String | Fan speed, one of <sup>3)</sup> `AUTO`, `SILENT`, `LEVEL1`, `LEVEL2`, `LEVEL3`, `LEVEL4`, `LEVEL5` | RW | `AC` |
| `swing`<sup>2)</sup> | Switch | Swing on/off | RW | `AC` |
| `verticalSwing`<sup>2)</sup> | String | Vertical swing state, one of <sup>3)</sup> `OFF`, `ON`, `UP`, `MID_UP`, `MID`, `MID_DOWN`, `DOWN`, `AUTO` | RW | `AC` |
| `horizontalSwing`<sup>2)</sup> | String | Horizontal swing state, one of <sup>3)</sup> `OFF`, `ON`, `LEFT`, `MID_LEFT`, `MID`, `MID_RIGHT`, `RIGHT`, `AUTO` | RW | `AC` |
| `batteryLowAlarm` | Switch | A control device in the Zone has a low battery | R | Any Zone |
| `openWindowDetected` | Switch | An open window has been detected in the Zone | R | `HEATING`, `AC` |
| `openWindowRemainingTime` | Number:Time | The remaining Open Window heating/cooling Override time in the Zone | R | `HEATING`, `AC` |
| `light` | Switch | State (`ON`, `OFF`) of the control panel light | RW | `AC` |
You will see some of the above mentioned Channels only if your tado° device supports the respective function.
@@ -147,7 +151,7 @@ In case the zone is currently in smart-schedule mode and thus doesn't have a ter
The `mobiledevice` thing represents a smart phone that is configured for tado°. It provides access to the geotracking functionality.
| Parameter | Required | Description | Default |
|-------------------|----------|----------------------------------------------|---------|
| ----------------- | -------- | -------------------------------------------- | ------- |
| `id` | yes | Mobile Device Id | - |
| `refreshInterval` | no | Refresh interval of state updates in seconds | 60 |
@@ -162,10 +166,10 @@ Bridge tado:home:demo [ username="mail@example.com", password="secret" ] {
}
```
### Items
### Channels
| Name | Type | Description | Read/Write |
|----------|--------|--------------------------------------------------|------------|
| -------- | ------ | ------------------------------------------------ | ---------- |
| `atHome` | Switch | ON if mobile device is in HOME mode, OFF if AWAY | R |
Group `OR` can be used to define an item for _'is any device at home'_.
@@ -44,6 +44,11 @@ public class TadoBindingConstants {
public static final String CHANNEL_HOME_PRESENCE_MODE = "homePresence";
public static final String CHANNEL_HOME_GEOFENCING_ENABLED = "geofencingEnabled";
public static final String CHANNEL_API_RATE_LIMIT = "apiRateLimit";
public static final String CHANNEL_API_RATE_DURATION = "apiRateDuration";
public static final String CHANNEL_API_RATE_REMAINING = "apiRateRemaining";
public static final String CHANNEL_API_RATE_RESET = "apiRateReset";
public static final String CHANNEL_ZONE_CURRENT_TEMPERATURE = "currentTemperature";
public static final String CHANNEL_ZONE_HUMIDITY = "humidity";
@@ -13,6 +13,7 @@
package org.openhab.binding.tado.internal.api;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.tado.internal.handler.TadoHomeHandler;
import org.openhab.binding.tado.swagger.codegen.api.GsonBuilderFactory;
import org.openhab.binding.tado.swagger.codegen.api.client.HomeApi;
import org.openhab.binding.tado.swagger.codegen.api.client.OAuthorizerV2;
@@ -29,9 +30,9 @@ import com.google.gson.Gson;
@NonNullByDefault
public class HomeApiFactory {
public HomeApi create(OAuthClientService oAuthClientService, String baseUrl) {
public HomeApi create(OAuthClientService oAuthClientService, String baseUrl, TadoHomeHandler homeHandler) {
Gson gson = GsonBuilderFactory.defaultGsonBuilder().create();
OAuthorizerV2 authorizer = new OAuthorizerV2(oAuthClientService);
return new HomeApi(gson, authorizer, baseUrl);
return new HomeApi(gson, authorizer, baseUrl, homeHandler);
}
}
@@ -20,8 +20,11 @@ import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import javax.measure.Unit;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.tado.internal.TadoBindingConstants;
import org.openhab.binding.tado.internal.TadoBindingConstants.TemperatureUnit;
import org.openhab.binding.tado.internal.api.HomeApiFactory;
import org.openhab.binding.tado.internal.config.TadoHomeConfig;
@@ -37,7 +40,10 @@ import org.openhab.binding.tado.swagger.codegen.api.model.UserHomes;
import org.openhab.core.auth.client.oauth2.AccessTokenRefreshListener;
import org.openhab.core.auth.client.oauth2.AccessTokenResponse;
import org.openhab.core.auth.client.oauth2.OAuthClientService;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.unit.Units;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
@@ -47,6 +53,7 @@ import org.openhab.core.thing.binding.BaseBridgeHandler;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.openhab.core.types.UnDefType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -101,7 +108,7 @@ public class TadoHomeHandler extends BaseBridgeHandler implements AccessTokenRef
OAuthClientService oAuthClientService = tadoHandlerFactory.subscribeOAuthClientService(this, user);
oAuthClientService.addAccessTokenRefreshListener(this);
this.api = new HomeApiFactory().create(oAuthClientService, configuration.tadoApiUrl);
this.api = new HomeApiFactory().create(oAuthClientService, configuration.tadoApiUrl, this);
this.oAuthClientService = oAuthClientService;
logger.trace("initialize() api v2 created");
confPendingText = CONF_PENDING_OAUTH_CREDS.formatted(TadoAuthenticationServlet.PATH,
@@ -287,4 +294,24 @@ public class TadoHomeHandler extends BaseBridgeHandler implements AccessTokenRef
public void onAccessTokenResponse(AccessTokenResponse atr) {
initializeBridgeStatusAndPropertiesIfOffline();
}
public void updateRateLimitInfo(int apiRateLimit, int apiRateDuration, int apiRateRemaining, int apiRateReset) {
updateAPIChannels(apiRateLimit, TadoBindingConstants.CHANNEL_API_RATE_LIMIT, Units.ONE);
updateAPIChannels(apiRateDuration, TadoBindingConstants.CHANNEL_API_RATE_DURATION, Units.SECOND);
updateAPIChannels(apiRateRemaining, TadoBindingConstants.CHANNEL_API_RATE_REMAINING, Units.ONE);
updateAPIChannels(apiRateReset, TadoBindingConstants.CHANNEL_API_RATE_RESET, Units.SECOND);
}
private void updateAPIChannels(int value, String channelName, Unit unit) {
if (value > -1) {
if (TadoBindingConstants.CHANNEL_API_RATE_LIMIT.equals(channelName)
|| TadoBindingConstants.CHANNEL_API_RATE_REMAINING.equals(channelName)) {
updateState(channelName, new DecimalType(value));
} else {
updateState(channelName, new QuantityType<>(value, unit));
}
} else {
updateState(channelName, UnDefType.UNDEF);
}
}
}
@@ -22,10 +22,12 @@ import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.client.util.StringContentProvider;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.openhab.binding.tado.internal.handler.TadoHomeHandler;
import org.openhab.binding.tado.swagger.codegen.api.ApiException;
import org.openhab.binding.tado.swagger.codegen.api.model.GenericZoneCapabilities;
import org.openhab.binding.tado.swagger.codegen.api.model.HomeInfo;
@@ -54,11 +56,55 @@ public class HomeApi {
private Gson gson;
private OAuthorizerV2 authorizer;
private final TadoHomeHandler homeHandler;
public HomeApi(Gson gson, OAuthorizerV2 authorizer, String baseUrl) {
private void extractRateLimitInfo(ContentResponse response) {
int apiRateLimit = -1;
int apiRateDuration = -1;
int apiRateRemaining = -1;
int apiRateReset = -1;
HttpFields headersfields = response.getHeaders();
String rateLimitPolicyValueString = headersfields.get("RateLimit-Policy");
if (rateLimitPolicyValueString != null) {
String[] rateLimitPolicyValues = rateLimitPolicyValueString.split(";");
for (String value : rateLimitPolicyValues) {
if (value.startsWith("q=")) {
apiRateLimit = safeParseInt(value.substring(2));
} else if (value.startsWith("w=")) {
apiRateDuration = safeParseInt(value.substring(2));
}
}
}
String rateLimitValueString = headersfields.get("RateLimit");
if (rateLimitValueString != null) {
String[] rateLimitValues = rateLimitValueString.split(";");
for (String value : rateLimitValues) {
if (value.startsWith("r=")) {
apiRateRemaining = safeParseInt(value.substring(2));
} else if (value.startsWith("w=")) { // some providers use 'w' as window/reset seconds
apiRateReset = safeParseInt(value.substring(2));
}
}
}
homeHandler.updateRateLimitInfo(apiRateLimit, apiRateDuration, apiRateRemaining, apiRateReset);
}
private static int safeParseInt(String s) {
try {
return Integer.parseInt(s.trim());
} catch (NumberFormatException e) {
return 0;
}
}
public HomeApi(Gson gson, OAuthorizerV2 authorizer, String baseUrl, TadoHomeHandler homeHandler) {
this.gson = gson;
this.authorizer = authorizer;
this.baseUrl = baseUrl;
this.homeHandler = homeHandler;
}
public void deleteZoneOverlay(Long homeId, Long zoneId) throws IOException, ApiException {
@@ -97,6 +143,7 @@ public class HomeApi {
}
int statusCode = response.getStatus();
extractRateLimitInfo(response);
if (statusCode >= HttpStatus.BAD_REQUEST_400) {
throw new ApiException(response, "Operation deleteZoneOverlay failed with error " + statusCode);
}
@@ -137,6 +184,7 @@ public class HomeApi {
Type returnType = new TypeToken<HomeState>() {
}.getType();
extractRateLimitInfo(response);
return gson.fromJson(response.getContentAsString(), returnType);
}
@@ -175,6 +223,7 @@ public class HomeApi {
Type returnType = new TypeToken<List<MobileDevice>>() {
}.getType();
extractRateLimitInfo(response);
return gson.fromJson(response.getContentAsString(), returnType);
}
@@ -213,6 +262,7 @@ public class HomeApi {
Type returnType = new TypeToken<List<Zone>>() {
}.getType();
extractRateLimitInfo(response);
return gson.fromJson(response.getContentAsString(), returnType);
}
@@ -251,6 +301,7 @@ public class HomeApi {
Type returnType = new TypeToken<HomeInfo>() {
}.getType();
extractRateLimitInfo(response);
return gson.fromJson(response.getContentAsString(), returnType);
}
@@ -284,6 +335,7 @@ public class HomeApi {
Type returnType = new TypeToken<User>() {
}.getType();
extractRateLimitInfo(response);
return gson.fromJson(response.getContentAsString(), returnType);
}
@@ -329,6 +381,7 @@ public class HomeApi {
Type returnType = new TypeToken<GenericZoneCapabilities>() {
}.getType();
extractRateLimitInfo(response);
return gson.fromJson(response.getContentAsString(), returnType);
}
@@ -374,6 +427,7 @@ public class HomeApi {
Type returnType = new TypeToken<OverlayTemplate>() {
}.getType();
extractRateLimitInfo(response);
return gson.fromJson(response.getContentAsString(), returnType);
}
@@ -419,6 +473,7 @@ public class HomeApi {
Type returnType = new TypeToken<Zone>() {
}.getType();
extractRateLimitInfo(response);
return gson.fromJson(response.getContentAsString(), returnType);
}
@@ -464,6 +519,7 @@ public class HomeApi {
Type returnType = new TypeToken<Overlay>() {
}.getType();
extractRateLimitInfo(response);
return gson.fromJson(response.getContentAsString(), returnType);
}
@@ -508,6 +564,7 @@ public class HomeApi {
Type returnType = new TypeToken<ZoneState>() {
}.getType();
extractRateLimitInfo(response);
return gson.fromJson(response.getContentAsString(), returnType);
}
@@ -548,6 +605,7 @@ public class HomeApi {
}
int statusCode = response.getStatus();
extractRateLimitInfo(response);
if (statusCode >= HttpStatus.BAD_REQUEST_400) {
throw new ApiException(response, "Operation updatePresenceLock failed with error " + statusCode);
}
@@ -603,6 +661,7 @@ public class HomeApi {
Type returnType = new TypeToken<Overlay>() {
}.getType();
extractRateLimitInfo(response);
return gson.fromJson(response.getContentAsString(), returnType);
}
@@ -43,6 +43,14 @@ thing-type.config.tado.zone.refreshInterval.description = Refresh interval of ho
channel-type.tado.acPower.label = Air-conditioning Power
channel-type.tado.acPower.description = Current power state of the air-conditioning
channel-type.tado.apiRateDuration.label = API Max Duration
channel-type.tado.apiRateDuration.description = Duration in which the API count rises until maximum
channel-type.tado.apiRateLimit.label = API Max
channel-type.tado.apiRateLimit.description = Maximum API calls allowed per specified duration
channel-type.tado.apiRateRemaining.label = API Calls remaining
channel-type.tado.apiRateRemaining.description = Number of API calls remaining before the maximum is reached
channel-type.tado.apiRateReset.label = Time until API count resets
channel-type.tado.apiRateReset.description = Time until the API count resets
channel-type.tado.atHome.label = At Home
channel-type.tado.atHome.description = ON if at home, OFF if away
channel-type.tado.currentTemperature.label = Temperature
@@ -13,6 +13,10 @@
<channels>
<channel typeId="homePresence" id="homePresence"/>
<channel typeId="geofencingEnabled" id="geofencingEnabled"/>
<channel typeId="apiRateLimit" id="apiRateLimit"/>
<channel typeId="apiRateDuration" id="apiRateDuration"/>
<channel typeId="apiRateRemaining" id="apiRateRemaining"/>
<channel typeId="apiRateReset" id="apiRateReset"/>
</channels>
<properties>
@@ -28,7 +32,7 @@
<parameter name="tadoApiUrl" type="text" required="true">
<label>API Base URL</label>
<description>he base URL used for all API requests. Can be changed to use a proxy.</description>
<description>The base URL used for all API requests. Can be changed to use a proxy.</description>
<default>https://my.tado.com/api/v2</default>
<advanced>true</advanced>
</parameter>
@@ -416,4 +420,48 @@
<state readOnly="true" pattern="%.0f %unit%"/>
</channel-type>
<channel-type id="apiRateDuration" advanced="true">
<item-type>Number:Time</item-type>
<label>API Max Duration</label>
<description>Duration in which the API count rises until maximum</description>
<category>Time</category>
<tags>
<tag>Status</tag>
<tag>Duration</tag>
</tags>
<state readOnly="true" pattern="%d %unit%"/>
</channel-type>
<channel-type id="apiRateRemaining" advanced="true">
<item-type>Number</item-type>
<label>API Calls remaining</label>
<description>Number of API calls remaining before the maximum is reached</description>
<tags>
<tag>Status</tag>
</tags>
<state readOnly="true" pattern="%d"/>
</channel-type>
<channel-type id="apiRateLimit" advanced="true">
<item-type>Number</item-type>
<label>API Max</label>
<description>Maximum API calls allowed per specified duration</description>
<tags>
<tag>Status</tag>
</tags>
<state readOnly="true" pattern="%d"/>
</channel-type>
<channel-type id="apiRateReset" advanced="true">
<item-type>Number:Time</item-type>
<label>Time until API count resets</label>
<description>Time until the API count resets</description>
<category>Time</category>
<tags>
<tag>Status</tag>
<tag>Duration</tag>
</tags>
<state readOnly="true" pattern="%d %unit%"/>
</channel-type>
</thing:thing-descriptions>