[tado] Code cleanup (#18691)

* [tado] delete old authentication and other unused code; fix deprecations

Signed-off-by: Andrew Fiddian-Green <software@whitebear.ch>
pull/18725/head
Andrew Fiddian-Green 2025-05-29 20:29:14 +02:00 committed by GitHub
parent 61fbe40073
commit 5da5401487
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 40 additions and 315 deletions

View File

@ -12,33 +12,29 @@ You can then monitor and control all zone types (Heating, AC, Hot Water) as well
The `home` thing serves as bridge to the tado° cloud services.
The binding will automatically discover this thing and place it in the Inbox.
It must be authenticated before it will actually go online.
There are two ways to authenticate it as follows:
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`.
1. Online via the OAuth Device Code Grant Flow (RFC-8628) authentication process through the link provided at `http://[openhab-ip-address]:8080/tado`.
1. Enter `username` and `password` credentials in the thing configuration parameters as shown in the table below.
| Parameter | Optional | Description |
|---------------|----------|-------------------------------------------------------------------------------|
| `rfcWithUser` | yes | Determines if the user name is included in the oAuth RFC-8628 authentication. |
| `username` | yes | Selects the tado° account to be used if there is more than one account. |
| `homeId` | yes | Selects the Home Id to use in case of more than one home per account. |
Note: after March 15th, 2025 online authentication is the tado° preferred (or even only) method.
In other words the `username` and `password` method has probably ceased to work after that date.
The `rfcWithUser` and `username` settings are only needed if you have more than one tado° account.
The `rfcWithUser` setting makes the binding use a different authentication token for each respective account `username`.
| Parameter | Optional | Description |
|---------------|----------|------------------------------------------------------------------------------------|
| `useRfc8628` | yes | Determines if the binding shall use oAuth RFC-8628 authentication |
| `rfcWithUser` | yes | Determines if the user name shall be included in the oAuth RFC-8628 authentication |
| `username` | yes | Username used to log in at [my.tado](https://my.tado.com) |
| `password` | yes | Password of the username |
| `homeId` | yes | Selects the Home Id to use (only needed if the account has multiple homes) |
The `rfcWithUser` setting is only needed if you have multiple tado° accounts.
It forces the binding to use different authentication tokens for each respective account `username`.
The `homeId` is only needed if you have multiple homes under a single tado° account.
The `homeId` is only needed if you have more than one home under a given tado° account.
It forces the binding to read and write the data for the respective Home Id.
If you do not have multiple homes, the binding always uses the first and only Home Id.
If you do not have multiple homes, the binding always uses the first and (therefore) only Home Id.
Example `tado.things`
```java
Bridge tado:home:demo [ username="mail@example.com", password="secret" ]
// normal example with one tado° account containing one home
Bridge tado:home:demo
..
// special case if you have more than one tado° account, or more than one home in an account
Bridge tado:home:demo [ rfcWithUser=true, username="mail@example.com", homeId=1234 ]
```
Once the `home` thing is online, the binding will discover all its respective zones and mobile devices, and place them in the Inbox.

View File

@ -12,8 +12,6 @@
*/
package org.openhab.binding.tado.internal.adapter;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.OffsetDateTime;
import org.eclipse.jdt.annotation.NonNullByDefault;
@ -227,11 +225,6 @@ public class TadoZoneStateAdapter {
return "ON".equals(power.getValue());
}
private static DecimalType toDecimalType(double value) {
BigDecimal decimal = new BigDecimal(value).setScale(2, RoundingMode.HALF_UP);
return new DecimalType(decimal);
}
private static DateTimeType toDateTimeType(OffsetDateTime offsetDateTime) {
return new DateTimeType(offsetDateTime.toInstant());
}

View File

@ -13,11 +13,9 @@
package org.openhab.binding.tado.internal.api;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.tado.internal.auth.OAuthorizerV2;
import org.openhab.binding.tado.swagger.codegen.api.GsonBuilderFactory;
import org.openhab.binding.tado.swagger.codegen.api.auth.Authorizer;
import org.openhab.binding.tado.swagger.codegen.api.auth.OAuthAuthorizer;
import org.openhab.binding.tado.swagger.codegen.api.client.HomeApi;
import org.openhab.binding.tado.swagger.codegen.api.client.OAuthorizerV2;
import org.openhab.core.auth.client.oauth2.OAuthClientService;
import com.google.gson.Gson;
@ -30,20 +28,10 @@ import com.google.gson.Gson;
*/
@NonNullByDefault
public class HomeApiFactory {
private static final String OAUTH_SCOPE = "home.user";
private static final String OAUTH_CLIENT_ID = "public-api-preview";
private static final String OAUTH_CLIENT_SECRET = "4HJGRffVR8xb3XdEUQpjgZ1VplJi6Xgw";
public HomeApi create(String username, String password) {
Gson gson = GsonBuilderFactory.defaultGsonBuilder().create();
Authorizer authorizer = new OAuthAuthorizer().passwordFlow(username, password).clientId(OAUTH_CLIENT_ID)
.clientSecret(OAUTH_CLIENT_SECRET).scopes(OAUTH_SCOPE);
return new HomeApi(gson, authorizer);
}
public HomeApi create(OAuthClientService oAuthClientService) {
Gson gson = GsonBuilderFactory.defaultGsonBuilder().create();
Authorizer authorizer = new OAuthorizerV2(oAuthClientService);
OAuthorizerV2 authorizer = new OAuthorizerV2(oAuthClientService);
return new HomeApi(gson, authorizer);
}
}

View File

@ -23,8 +23,6 @@ import org.eclipse.jdt.annotation.Nullable;
@NonNullByDefault
public class TadoHomeConfig {
public @Nullable String username;
public @Nullable String password;
public @Nullable Boolean useRfc8628;
public @Nullable Boolean rfcWithUser;
public @Nullable Integer homeId;
}

View File

@ -15,9 +15,7 @@ package org.openhab.binding.tado.internal.handler;
import static org.openhab.binding.tado.internal.TadoBindingConstants.*;
import java.io.IOException;
import java.time.ZonedDateTime;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
@ -39,7 +37,6 @@ 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.config.core.Configuration;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.ChannelUID;
@ -65,13 +62,9 @@ public class TadoHomeHandler extends BaseBridgeHandler implements AccessTokenRef
// thing status description i18n text pointers
private static final String CONF_ERROR_NO_HOME = "@text/tado.home.status.nohome";
private static final String CONF_ERROR_NO_HOME_ID = "@text/tado.home.status.nohomeid";
private static final String CONF_PENDING_USER_CREDS = "@text/tado.home.status.username";
private static final String CONF_PENDING_OAUTH_CREDS = //
"@text/tado.home.status.oauth [\"http(s)://<YOUROPENHAB>:<YOURPORT>%s?%s=%s\"]";
// tado specific RFC-8628 oAuth authentication parameters
private static final ZonedDateTime OAUTH_MANDATORY_FROM_DATE = ZonedDateTime.parse("2025-03-15T00:00:00Z");
private final Logger logger = LoggerFactory.getLogger(TadoHomeHandler.class);
private final TadoBatteryChecker batteryChecker;
@ -102,37 +95,17 @@ public class TadoHomeHandler extends BaseBridgeHandler implements AccessTokenRef
@Override
public void initialize() {
configuration = getConfigAs(TadoHomeConfig.class);
String user = Boolean.TRUE.equals(configuration.rfcWithUser)
? configuration.username instanceof String name && !name.isBlank() ? name : null
: null;
String username = configuration.username;
String password = configuration.password;
boolean v1CredentialsMissing = username == null || username.isBlank() || password == null || password.isBlank();
boolean suggestRfc8628 = false;
suggestRfc8628 |= Boolean.TRUE.equals(configuration.useRfc8628);
suggestRfc8628 |= v1CredentialsMissing;
suggestRfc8628 |= ZonedDateTime.now().isAfter(OAUTH_MANDATORY_FROM_DATE);
if (suggestRfc8628) {
String rfcUser = Boolean.TRUE.equals(configuration.rfcWithUser) //
? username != null && !username.isBlank() ? username : null
: null;
OAuthClientService oAuthClientService = tadoHandlerFactory.subscribeOAuthClientService(this, rfcUser);
oAuthClientService.addAccessTokenRefreshListener(this);
this.api = new HomeApiFactory().create(oAuthClientService);
this.oAuthClientService = oAuthClientService;
logger.trace("initialize() api v2 created");
confPendingText = CONF_PENDING_OAUTH_CREDS.formatted(TadoAuthenticationServlet.PATH,
TadoAuthenticationServlet.PARAM_NAME_USER, rfcUser != null ? rfcUser : "");
if (!Boolean.TRUE.equals(configuration.useRfc8628)) {
Configuration configuration = editConfiguration();
configuration.put(CONFIG_USE_RFC8628, Boolean.TRUE);
updateConfiguration(configuration);
}
} else {
api = new HomeApiFactory().create(Objects.requireNonNull(username), Objects.requireNonNull(password));
logger.trace("initialize() api v1 created");
confPendingText = CONF_PENDING_USER_CREDS;
}
OAuthClientService oAuthClientService = tadoHandlerFactory.subscribeOAuthClientService(this, user);
oAuthClientService.addAccessTokenRefreshListener(this);
this.api = new HomeApiFactory().create(oAuthClientService);
this.oAuthClientService = oAuthClientService;
logger.trace("initialize() api v2 created");
confPendingText = CONF_PENDING_OAUTH_CREDS.formatted(TadoAuthenticationServlet.PATH,
TadoAuthenticationServlet.PARAM_NAME_USER, user != null ? user : "");
ScheduledFuture<?> initializationFuture = this.initializationFuture;
if (initializationFuture == null || initializationFuture.isDone()) {

View File

@ -86,7 +86,7 @@ public final class RuntimeTypeAdapterFactory<T> implements TypeAdapterFactory {
return new TypeAdapter<R>() {
@Override
public R read(JsonReader in) throws IOException {
JsonElement jsonElement = new JsonParser().parse(in);
JsonElement jsonElement = JsonParser.parseReader(in);
JsonElement labelJsonElement = jsonElement.getAsJsonObject().get(typeFieldName);
TypeAdapter<R> delegate = null;

View File

@ -1,27 +0,0 @@
/*
* Copyright (c) 2010-2025 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.tado.swagger.codegen.api.auth;
import java.io.IOException;
import org.eclipse.jetty.client.api.Request;
import org.openhab.binding.tado.swagger.codegen.api.ApiException;
/**
* Static imported copy of the Java file originally created by Swagger Codegen.
*
* @author Andrew Fiddian-Green - Initial contribution
*/
public interface Authorizer {
void addAuthorization(Request request) throws ApiException, IOException;
}

View File

@ -1,177 +0,0 @@
/*
* Copyright (c) 2010-2025 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.tado.swagger.codegen.api.auth;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.client.api.Request;
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.swagger.codegen.api.ApiException;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
/**
* Static imported copy of the Java file originally created by Swagger Codegen.
*
* @author Andrew Fiddian-Green - Initial contribution
*/
public class OAuthAuthorizer implements Authorizer {
private static final String GRANT_TYPE_PASSWORD = "password";
private static final int TOKEN_GRACE_PERIOD = 30;
private static final HttpClient CLIENT = new HttpClient(new SslContextFactory());
private final Gson gson = new GsonBuilder().create();
private String tokenUrl = "https://auth.tado.com/oauth/token";
private String grantType;
private String username;
private String password;
private String clientId;
private String clientSecret;
private String scope;
private String accessToken;
private String refreshToken;
private LocalDateTime tokenExpiration;
public OAuthAuthorizer() {
}
public OAuthAuthorizer passwordFlow(String username, String password) {
this.grantType = "password";
this.username = username;
this.password = password;
return this;
}
public OAuthAuthorizer tokenUrl(String tokenUrl) {
this.tokenUrl = tokenUrl;
return this;
}
public OAuthAuthorizer clientId(String clientId) {
this.clientId = clientId;
return this;
}
public OAuthAuthorizer clientSecret(String clientSecret) {
this.clientSecret = clientSecret;
return this;
}
public OAuthAuthorizer scopes(String... scopes) {
this.scope = String.join(" ", scopes);
return this;
}
private void initializeTokens() throws IOException {
startHttpClient(CLIENT);
List<String> queryParams = new ArrayList<>();
if (this.refreshToken != null) {
queryParams.add(queryParam("grant_type", "refresh_token"));
queryParams.add(queryParam("refresh_token", this.refreshToken));
} else if (GRANT_TYPE_PASSWORD.equals(this.grantType)) {
queryParams.add(queryParam("grant_type", this.grantType));
queryParams.add(queryParam("username", this.username));
queryParams.add(queryParam("password", this.password));
queryParams.add(queryParam("scope", this.scope));
}
if (this.clientId != null) {
queryParams.add(queryParam("client_id", this.clientId));
}
if (this.clientSecret != null) {
queryParams.add(queryParam("client_secret", this.clientSecret));
}
Request request = CLIENT.newRequest(this.tokenUrl + "?" + String.join("&", queryParams)).method(HttpMethod.POST)
.timeout(5, TimeUnit.SECONDS);
request.header(HttpHeader.USER_AGENT, "openhab/swagger-java/1.0.0");
try {
ContentResponse response = request.send();
if (response.getStatus() == HttpStatus.OK_200) {
Map<?, ?> tokenValues = gson.fromJson(response.getContentAsString(), Map.class);
this.accessToken = (String) tokenValues.get("access_token");
this.refreshToken = (String) tokenValues.get("refresh_token");
this.tokenExpiration = LocalDateTime.now().plusSeconds(
Double.valueOf(tokenValues.get("expires_in").toString()).longValue() - TOKEN_GRACE_PERIOD);
} else {
this.accessToken = null;
this.refreshToken = null;
this.tokenExpiration = null;
throw new ApiException(response, "Error getting access token");
}
} catch (Exception e) {
throw new IOException("Error calling " + this.tokenUrl, e);
}
}
private String queryParam(String key, String value) {
try {
return key + "=" + URLEncoder.encode(value, "UTF-8");
} catch (UnsupportedEncodingException e) {
return key + "=" + value;
}
}
private boolean isExpired() {
return this.tokenExpiration == null || this.tokenExpiration.isBefore(LocalDateTime.now());
}
public String getToken() throws IOException {
if (accessToken == null || this.isExpired()) {
synchronized (this) {
if (accessToken == null || this.isExpired()) {
initializeTokens();
}
}
}
return this.accessToken;
}
@Override
public void addAuthorization(Request request) throws IOException {
request.header(HttpHeader.AUTHORIZATION, "Bearer " + getToken());
}
private static void startHttpClient(HttpClient client) {
if (!client.isStarted()) {
try {
client.start();
} catch (Exception e) {
// nothing we can do here
}
}
}
}

View File

@ -26,9 +26,7 @@ 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.auth.OAuthorizerV2;
import org.openhab.binding.tado.swagger.codegen.api.ApiException;
import org.openhab.binding.tado.swagger.codegen.api.auth.Authorizer;
import org.openhab.binding.tado.swagger.codegen.api.model.GenericZoneCapabilities;
import org.openhab.binding.tado.swagger.codegen.api.model.HomeInfo;
import org.openhab.binding.tado.swagger.codegen.api.model.HomePresence;
@ -49,15 +47,15 @@ import com.google.gson.reflect.TypeToken;
* @author Andrew Fiddian-Green - Initial contribution
*/
public class HomeApi {
private static final HttpClient CLIENT = new HttpClient(new SslContextFactory());
private static final HttpClient CLIENT = new HttpClient(new SslContextFactory.Client());
private String baseUrl = "https://my.tado.com/api/v2";
private int timeout = 5000;
private Gson gson;
private Authorizer authorizer;
private OAuthorizerV2 authorizer;
public HomeApi(Gson gson, Authorizer authorizer) {
public HomeApi(Gson gson, OAuthorizerV2 authorizer) {
this.gson = gson;
this.authorizer = authorizer;
}

View File

@ -10,13 +10,12 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.tado.internal.auth;
package org.openhab.binding.tado.swagger.codegen.api.client;
import java.io.IOException;
import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.http.HttpHeader;
import org.openhab.binding.tado.swagger.codegen.api.auth.Authorizer;
import org.openhab.core.auth.client.oauth2.AccessTokenResponse;
import org.openhab.core.auth.client.oauth2.OAuthClientService;
import org.openhab.core.auth.client.oauth2.OAuthException;
@ -25,7 +24,9 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This is a new {@link Authorizer} that is mandated by Tado after March 15 2025.
* This is a new authorizer that was mandated by Tado after March 15 2025.
* <p>
* Based on static imported copy of class created by Swagger Codegen
*
* @see <a href="https://support.tado.com/en/articles/8565472-how-do-i-authenticate-to-access-the-rest-api">Tado Support
* Article</a>
@ -33,7 +34,7 @@ import org.slf4j.LoggerFactory;
*
* @author Andrew Fiddian-Green - Initial contribution
*/
public class OAuthorizerV2 implements Authorizer {
public class OAuthorizerV2 {
private final Logger logger = LoggerFactory.getLogger(OAuthorizerV2.class);
@ -43,7 +44,6 @@ public class OAuthorizerV2 implements Authorizer {
this.oAuthService = oAuthService;
}
@Override
public void addAuthorization(Request request) {
try {
AccessTokenResponse token = oAuthService.getAccessTokenResponse();

View File

@ -19,15 +19,11 @@ thing-type.tado.zone.channel.humidity.description = Current humidity in %
# thing types config
thing-type.config.tado.home.homeId.label = Home Id
thing-type.config.tado.home.homeId.description = Selects the Home Id to be used (only needed if there are multiple homes)
thing-type.config.tado.home.password.label = Password
thing-type.config.tado.home.password.description = Password of tado login used for API access
thing-type.config.tado.home.homeId.description = Selects the Home Id to be used if there is more than one home per account.
thing-type.config.tado.home.rfcWithUser.label = RFC-8628 with User
thing-type.config.tado.home.rfcWithUser.description = Determines if the user name is included in the oAuth RFC-8628 authentication
thing-type.config.tado.home.useRfc8628.label = Use oAuth RFC-8628
thing-type.config.tado.home.useRfc8628.description = Determines if the binding shall use oAuth RFC-8628 authentication
thing-type.config.tado.home.username.label = User Name
thing-type.config.tado.home.username.description = User name of tado login used for API access
thing-type.config.tado.home.username.description = Selects the tado° account to be used if there is more than one account.
thing-type.config.tado.mobiledevice.id.label = Mobile Device Id
thing-type.config.tado.mobiledevice.id.description = Id of the mobile device
thing-type.config.tado.mobiledevice.refreshInterval.label = Refresh Interval
@ -123,7 +119,6 @@ channel-type.tado.verticalSwing.state.option.OFF = OFF
# tado home thing status messages
tado.home.status.oauth = Try authenticating at {0}
tado.home.status.username = Username and/or password might be invalid
tado.home.status.nohome = User does not have access to any home
tado.home.status.nohomeid = Missing Home Id

View File

@ -20,11 +20,6 @@
</properties>
<config-description>
<parameter name="useRfc8628" type="boolean" required="false">
<label>Use oAuth RFC-8628</label>
<description>Determines if the binding shall use oAuth RFC-8628 authentication</description>
</parameter>
<parameter name="rfcWithUser" type="boolean" required="false">
<label>RFC-8628 with User</label>
<description>Determines if the user name is included in the oAuth RFC-8628 authentication</description>
@ -33,20 +28,13 @@
<parameter name="username" type="text" required="false">
<label>User Name</label>
<description>User name of tado login used for API access</description>
<advanced>true</advanced>
</parameter>
<parameter name="password" type="text" required="false">
<label>Password</label>
<description>Password of tado login used for API access</description>
<context>password</context>
<description>Selects the tado° account to be used if there is more than one account.</description>
<advanced>true</advanced>
</parameter>
<parameter name="homeId" type="integer" required="false">
<label>Home Id</label>
<description>Selects the Home Id to be used (only needed if there are multiple homes)</description>
<description>Selects the Home Id to be used if there is more than one home per account.</description>
<advanced>true</advanced>
</parameter>
</config-description>