From e2e1c882a233d8eb83eb5205872b0e50487c1cef Mon Sep 17 00:00:00 2001 From: Andrew Fiddian-Green Date: Tue, 1 Apr 2025 16:14:29 +0100 Subject: [PATCH 1/7] [tado] add support for multiple accounts / homes Signed-off-by: Andrew Fiddian-Green --- .../tado/internal/config/TadoHomeConfig.java | 2 + .../internal/handler/TadoHandlerFactory.java | 58 ++++++++++++------- .../internal/handler/TadoHomeHandler.java | 32 ++++++++-- .../servlet/TadoAuthenticationServlet.java | 20 ++++--- .../resources/OH-INF/i18n/tado.properties | 9 +++ .../resources/OH-INF/thing/thing-types.xml | 10 ++++ 6 files changed, 98 insertions(+), 33 deletions(-) diff --git a/bundles/org.openhab.binding.tado/src/main/java/org/openhab/binding/tado/internal/config/TadoHomeConfig.java b/bundles/org.openhab.binding.tado/src/main/java/org/openhab/binding/tado/internal/config/TadoHomeConfig.java index dd8b666764565..2a734024c6389 100644 --- a/bundles/org.openhab.binding.tado/src/main/java/org/openhab/binding/tado/internal/config/TadoHomeConfig.java +++ b/bundles/org.openhab.binding.tado/src/main/java/org/openhab/binding/tado/internal/config/TadoHomeConfig.java @@ -25,4 +25,6 @@ public class TadoHomeConfig { public @Nullable String username; public @Nullable String password; public @Nullable Boolean useRfc8628; + public @Nullable Boolean rfcWithAccount; + public @Nullable Integer homeId; } diff --git a/bundles/org.openhab.binding.tado/src/main/java/org/openhab/binding/tado/internal/handler/TadoHandlerFactory.java b/bundles/org.openhab.binding.tado/src/main/java/org/openhab/binding/tado/internal/handler/TadoHandlerFactory.java index 4bb7833c3fd83..5086dcc722303 100644 --- a/bundles/org.openhab.binding.tado/src/main/java/org/openhab/binding/tado/internal/handler/TadoHandlerFactory.java +++ b/bundles/org.openhab.binding.tado/src/main/java/org/openhab/binding/tado/internal/handler/TadoHandlerFactory.java @@ -20,6 +20,7 @@ import java.util.Hashtable; import java.util.Map; import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; import javax.servlet.ServletException; @@ -73,14 +74,13 @@ public class TadoHandlerFactory extends BaseThingHandlerFactory { private final Logger logger = LoggerFactory.getLogger(TadoHandlerFactory.class); private final Set oAuthClientServiceSubscribers = new HashSet<>(); private final Map> discoveryServiceRegs = new HashMap<>(); + private final Map oAuthClientServices = new ConcurrentHashMap<>(); private final TadoStateDescriptionProvider stateDescriptionProvider; private final HttpService httpService; private final OAuthFactory oAuthFactory; private final TadoAuthenticationServlet httpServlet; - private @Nullable OAuthClientService oAuthClientService; - @Activate public TadoHandlerFactory(@Reference TadoStateDescriptionProvider stateDescriptionProvider, @Reference HttpService httpService, @Reference OAuthFactory oAuthFactory) { @@ -92,9 +92,8 @@ public TadoHandlerFactory(@Reference TadoStateDescriptionProvider stateDescripti @Deactivate public void deactivate() { - if (oAuthClientService != null) { - oAuthFactory.ungetOAuthService(THING_TYPE_HOME.toString()); - } + oAuthClientServices.keySet().forEach(id -> oAuthFactory.ungetOAuthService(id)); + oAuthClientServices.clear(); } @Override @@ -107,7 +106,7 @@ public boolean supportsThingType(ThingTypeUID thingTypeUID) { ThingTypeUID thingTypeUID = thing.getThingTypeUID(); if (thingTypeUID.equals(THING_TYPE_HOME)) { - TadoHomeHandler tadoHomeHandler = new TadoHomeHandler((Bridge) thing, this, oAuthFactory); + TadoHomeHandler tadoHomeHandler = new TadoHomeHandler((Bridge) thing, this); registerTadoDiscoveryService(tadoHomeHandler); return tadoHomeHandler; } else if (thingTypeUID.equals(THING_TYPE_ZONE)) { @@ -148,10 +147,11 @@ protected synchronized void removeHandler(ThingHandler thingHandler) { * Retrieves the pre-existing {@link OAuthClientService} if present, or creates a new one. * If necessary also registers the {@link TadoAuthenticationServlet}. * - * @param tadoHomeHandler + * @param tadoHomeHandler the subscribing thing handler + * @param account the optional account name (may be null) * @return an {@link OAuthClientService} */ - public OAuthClientService subscribeOAuthClientService(TadoHomeHandler tadoHomeHandler) { + public OAuthClientService subscribeOAuthClientService(TadoHomeHandler tadoHomeHandler, @Nullable String account) { if (oAuthClientServiceSubscribers.isEmpty()) { try { httpService.registerServlet(TadoAuthenticationServlet.PATH, httpServlet, null, null); @@ -162,16 +162,18 @@ public OAuthClientService subscribeOAuthClientService(TadoHomeHandler tadoHomeHa oAuthClientServiceSubscribers.add(tadoHomeHandler); - OAuthClientService oAuthClientService = this.oAuthClientService; + OAuthClientService oAuthClientService = oAuthClientServices.get(getServiceId(account)); if (oAuthClientService == null) { - oAuthClientService = oAuthFactory.getOAuthClientService(THING_TYPE_HOME.toString()); - this.oAuthClientService = oAuthClientService; + oAuthClientService = oAuthFactory.getOAuthClientService(getServiceId(account)); + if (oAuthClientService != null) { + oAuthClientServices.put(getServiceId(account), oAuthClientService); + } } if (oAuthClientService == null) { - oAuthClientService = oAuthFactory.createOAuthClientService(THING_TYPE_HOME.toString(), // - OAUTH_TOKEN_URL, OAUTH_DEVICE_URL, OAUTH_CLIENT_ID, null, OAUTH_SCOPE, false); - this.oAuthClientService = oAuthClientService; + oAuthClientService = oAuthFactory.createOAuthClientService(getServiceId(account), OAUTH_TOKEN_URL, + OAUTH_DEVICE_URL, OAUTH_CLIENT_ID, null, OAUTH_SCOPE, false); + oAuthClientServices.put(getServiceId(account), oAuthClientService); } return oAuthClientService; @@ -193,11 +195,12 @@ public void unsubscribeOAuthClientService(TadoHomeHandler tadoHomeHandler) { /** * Returns a nullable {@link AccessTokenResponse} if the OAuthClientService exists. * + * @param account the optional account name (may be null) * @return a nullable {@link AccessTokenResponse}. * @throws OAuthException on any error */ - public @Nullable AccessTokenResponse getAccessTokenResponse() throws OAuthException { - OAuthClientService oAuthClientService = this.oAuthClientService; + public @Nullable AccessTokenResponse getAccessTokenResponse(@Nullable String account) throws OAuthException { + OAuthClientService oAuthClientService = oAuthClientServices.get(getServiceId(account)); if (oAuthClientService == null) { throw new OAuthException("Missing OAuthClientService"); } @@ -212,11 +215,12 @@ public void unsubscribeOAuthClientService(TadoHomeHandler tadoHomeHandler) { /** * Returns a non null DeviceCodeResponse from the OAuthClientService if it exists. * + * @param account the optional account name (may be null) * @return a {@link DeviceCodeResponseDTO} * @throws OAuthException if it cannot return a non null result */ - public DeviceCodeResponseDTO getDeviceCodeResponse() throws OAuthException { - OAuthClientService oAuthClientService = this.oAuthClientService; + public DeviceCodeResponseDTO getDeviceCodeResponse(@Nullable String account) throws OAuthException { + OAuthClientService oAuthClientService = oAuthClientServices.get(getServiceId(account)); if (oAuthClientService == null) { throw new OAuthException("Missing OAuthClientService"); } @@ -227,7 +231,21 @@ public DeviceCodeResponseDTO getDeviceCodeResponse() throws OAuthException { return result; } - public boolean hasOAuthClientService() { - return oAuthClientService != null; + /** + * Check if there is an OAuthClientService registered + * + * @param account the optional account name (may be null) + */ + public boolean hasOAuthClientService(@Nullable String account) { + return oAuthClientServices.containsKey(getServiceId(account)); + } + + /** + * Build a unique OAuth service id using the (optional) account if present and not blank + * + * @param account the optional account name (may be null) + */ + private String getServiceId(@Nullable String account) { + return THING_TYPE_HOME.toString() + (account != null && !account.isBlank() ? ":" + account : ""); } } diff --git a/bundles/org.openhab.binding.tado/src/main/java/org/openhab/binding/tado/internal/handler/TadoHomeHandler.java b/bundles/org.openhab.binding.tado/src/main/java/org/openhab/binding/tado/internal/handler/TadoHomeHandler.java index 0fc78a51f1c49..a68a35e223678 100644 --- a/bundles/org.openhab.binding.tado/src/main/java/org/openhab/binding/tado/internal/handler/TadoHomeHandler.java +++ b/bundles/org.openhab.binding.tado/src/main/java/org/openhab/binding/tado/internal/handler/TadoHomeHandler.java @@ -20,6 +20,7 @@ import java.util.Objects; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; @@ -38,7 +39,6 @@ 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.auth.client.oauth2.OAuthFactory; import org.openhab.core.library.types.OnOffType; import org.openhab.core.thing.Bridge; import org.openhab.core.thing.ChannelUID; @@ -64,7 +64,8 @@ public class TadoHomeHandler extends BaseBridgeHandler implements AccessTokenRef 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)://:%s\"]"; + private static final String CONF_PENDING_OAUTH_CREDS = // + "@text/tado.home.status.oauth [\"http(s)://:%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"); @@ -82,7 +83,7 @@ public class TadoHomeHandler extends BaseBridgeHandler implements AccessTokenRef private @Nullable ScheduledFuture initializationFuture; private @Nullable OAuthClientService oAuthClientService; - public TadoHomeHandler(Bridge bridge, TadoHandlerFactory tadoHandlerFactory, OAuthFactory oAuthFactory) { + public TadoHomeHandler(Bridge bridge, TadoHandlerFactory tadoHandlerFactory) { super(bridge); this.batteryChecker = new TadoBatteryChecker(this); this.configuration = getConfigAs(TadoHomeConfig.class); @@ -108,12 +109,16 @@ public void initialize() { suggestRfc8628 |= ZonedDateTime.now().isAfter(OAUTH_MANDATORY_FROM_DATE); if (suggestRfc8628) { - OAuthClientService oAuthClientService = tadoHandlerFactory.subscribeOAuthClientService(this); + String account = Boolean.TRUE.equals(configuration.rfcWithAccount) // + ? userName != null && !userName.isBlank() ? userName : null + : null; + OAuthClientService oAuthClientService = tadoHandlerFactory.subscribeOAuthClientService(this, account); 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); + confPendingText = CONF_PENDING_OAUTH_CREDS.formatted(TadoAuthenticationServlet.PATH, + TadoAuthenticationServlet.PARAM_NAME_ACCOUNT, account != null ? account : ""); } else { api = new HomeApiFactory().create(Objects.requireNonNull(userName), Objects.requireNonNull(password)); logger.trace("initialize() api v1 created"); @@ -154,7 +159,24 @@ private synchronized void initializeBridgeStatusAndPropertiesIfOffline() { return; } + /* + * If there is only one home, or if there is no valid configuration.homeId entry, then use the first + * home id. Otherwise use the configuration.homeId entry value (if one exists). If there is no valid + * configuration.homeId entry but there are multiple homes then log the available home to help the + * user set up the proper configuration.homeId entry. + */ Integer firstHomeId = homes.get(0).getId(); + if (homes.size() > 1) { + Integer configHomeId = configuration.homeId; + if (configHomeId == null || configHomeId == 0) { + logger.info("Trying first Home Id in the list [{}]", homes.stream() + .map(home -> String.valueOf(home.getId())).collect(Collectors.joining(","))); + } else { + firstHomeId = homes.stream().map(home -> home.getId()).filter(id -> configHomeId.equals(id)) + .findFirst().orElse(null); + } + } + if (firstHomeId == null) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, CONF_ERROR_NO_HOME_ID); return; diff --git a/bundles/org.openhab.binding.tado/src/main/java/org/openhab/binding/tado/internal/servlet/TadoAuthenticationServlet.java b/bundles/org.openhab.binding.tado/src/main/java/org/openhab/binding/tado/internal/servlet/TadoAuthenticationServlet.java index 9835fc2eee59e..1e98f7f9a001c 100644 --- a/bundles/org.openhab.binding.tado/src/main/java/org/openhab/binding/tado/internal/servlet/TadoAuthenticationServlet.java +++ b/bundles/org.openhab.binding.tado/src/main/java/org/openhab/binding/tado/internal/servlet/TadoAuthenticationServlet.java @@ -43,8 +43,9 @@ public class TadoAuthenticationServlet extends HttpServlet { private static final String HTML_AUTH_ERROR_TEMPLATE = "$REPLACE$"; private static final String HTML_AUTH_START_TEMPLATE = "click to authenticate"; - private static final String PARAM_NAME = "oauth"; - private static final String PARAM_VALUE = "start"; + public static final String PARAM_NAME_ACCOUNT = "account"; + private static final String PARAM_NAME_OAUTH = "oauth"; + private static final String PARAM_VALUE_START = "start"; private static final String ERROR_BAD_URL = "no verification uri"; @@ -76,8 +77,9 @@ public TadoAuthenticationServlet(TadoHandlerFactory tadoHandlerFactory) { @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { - // if the query string is "?oauth=start" then serve the user authentication page - if (PARAM_VALUE.equals(request.getParameter(PARAM_NAME)) && tadoHandlerFactory.hasOAuthClientService()) { + // if the query string contains "oauth=start" then serve the user authentication page + if (PARAM_VALUE_START.equals(request.getParameter(PARAM_NAME_OAUTH)) + && tadoHandlerFactory.hasOAuthClientService(request.getParameter(PARAM_NAME_ACCOUNT))) { serveUserAuthenticationPage(request, response); } else { serveStatusPage(request, response); @@ -100,13 +102,13 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response) private void serveStatusPage(HttpServletRequest request, HttpServletResponse response) throws IOException { String dynamicHtml = null; - if (!tadoHandlerFactory.hasOAuthClientService()) { + if (!tadoHandlerFactory.hasOAuthClientService(request.getParameter(PARAM_NAME_ACCOUNT))) { dynamicHtml = HTML_AUTH_NOT_REQUIRED; } if (dynamicHtml == null) { try { - if (tadoHandlerFactory.getAccessTokenResponse() != null) { + if (tadoHandlerFactory.getAccessTokenResponse(request.getParameter(PARAM_NAME_ACCOUNT)) != null) { dynamicHtml = HTML_AUTH_PASSED; } } catch (OAuthException e) { @@ -117,7 +119,8 @@ private void serveStatusPage(HttpServletRequest request, HttpServletResponse res if (dynamicHtml == null) { if (request.getRequestURL() instanceof StringBuffer baseUrl) { - String dynamicUrl = baseUrl.append("?").append(PARAM_NAME).append("=").append(PARAM_VALUE).toString(); + String dynamicUrl = baseUrl.append("?").append(PARAM_NAME_OAUTH).append("=").append(PARAM_VALUE_START) + .toString(); dynamicHtml = HTML_AUTH_START_TEMPLATE.replace(REPLACE_TAG, dynamicUrl); } else { dynamicHtml = HTML_AUTH_ERROR_TEMPLATE.replace(REPLACE_TAG, ERROR_BAD_URL); @@ -145,7 +148,8 @@ private void serveUserAuthenticationPage(HttpServletRequest request, HttpServlet String dynamicHtml = null; try { - DeviceCodeResponseDTO deviceCodeResponse = tadoHandlerFactory.getDeviceCodeResponse(); + DeviceCodeResponseDTO deviceCodeResponse = tadoHandlerFactory + .getDeviceCodeResponse(request.getParameter(PARAM_NAME_ACCOUNT)); String userVerificationUri = deviceCodeResponse.getVerificationUriComplete(); if (userVerificationUri != null && !userVerificationUri.isBlank()) { response.sendRedirect(userVerificationUri); diff --git a/bundles/org.openhab.binding.tado/src/main/resources/OH-INF/i18n/tado.properties b/bundles/org.openhab.binding.tado/src/main/resources/OH-INF/i18n/tado.properties index adf8b766982b6..beb95bf8f22b6 100644 --- a/bundles/org.openhab.binding.tado/src/main/resources/OH-INF/i18n/tado.properties +++ b/bundles/org.openhab.binding.tado/src/main/resources/OH-INF/i18n/tado.properties @@ -18,8 +18,12 @@ 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.rfcWithAccount.label = RFC-8628 With Account +thing-type.config.tado.home.rfcWithAccount.description = Determines if the binding shall include the account in 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 @@ -114,6 +118,11 @@ channel-type.tado.verticalSwing.state.option.DOWN = DOWN channel-type.tado.verticalSwing.state.option.ON = ON channel-type.tado.verticalSwing.state.option.OFF = OFF +# thing types config + +thing-type.config.tado.home.withAccount.label = OAuth With Account +thing-type.config.tado.home.withAccount.description = Determines if the binding shall include the account name in the oAuth service id + # tado home thing status messages tado.home.status.oauth = Try authenticating at {0} diff --git a/bundles/org.openhab.binding.tado/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.tado/src/main/resources/OH-INF/thing/thing-types.xml index 7df199103117a..50dab43c7cdb6 100644 --- a/bundles/org.openhab.binding.tado/src/main/resources/OH-INF/thing/thing-types.xml +++ b/bundles/org.openhab.binding.tado/src/main/resources/OH-INF/thing/thing-types.xml @@ -19,6 +19,11 @@ Determines if the binding shall use oAuth RFC-8628 authentication + + + Determines if the binding shall include the account in oAuth RFC-8628 authentication + + User name of tado login used for API access @@ -31,6 +36,11 @@ password true + + + + Selects the Home Id to be used (only needed if there are multiple homes) + From 5216f84f7ab72b11d537240822cb21cd92cf1b76 Mon Sep 17 00:00:00 2001 From: Andrew Fiddian-Green Date: Tue, 1 Apr 2025 16:32:54 +0100 Subject: [PATCH 2/7] fix url Signed-off-by: Andrew Fiddian-Green --- .../tado/internal/servlet/TadoAuthenticationServlet.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bundles/org.openhab.binding.tado/src/main/java/org/openhab/binding/tado/internal/servlet/TadoAuthenticationServlet.java b/bundles/org.openhab.binding.tado/src/main/java/org/openhab/binding/tado/internal/servlet/TadoAuthenticationServlet.java index 1e98f7f9a001c..498b89f3152ad 100644 --- a/bundles/org.openhab.binding.tado/src/main/java/org/openhab/binding/tado/internal/servlet/TadoAuthenticationServlet.java +++ b/bundles/org.openhab.binding.tado/src/main/java/org/openhab/binding/tado/internal/servlet/TadoAuthenticationServlet.java @@ -120,7 +120,8 @@ private void serveStatusPage(HttpServletRequest request, HttpServletResponse res if (dynamicHtml == null) { if (request.getRequestURL() instanceof StringBuffer baseUrl) { String dynamicUrl = baseUrl.append("?").append(PARAM_NAME_OAUTH).append("=").append(PARAM_VALUE_START) - .toString(); + .append("&").append(PARAM_NAME_ACCOUNT).append("=") + .append(request.getParameter(PARAM_NAME_ACCOUNT)).toString(); dynamicHtml = HTML_AUTH_START_TEMPLATE.replace(REPLACE_TAG, dynamicUrl); } else { dynamicHtml = HTML_AUTH_ERROR_TEMPLATE.replace(REPLACE_TAG, ERROR_BAD_URL); From c88a55f4c63f02352f4c286d888a0c38f0e9ecf2 Mon Sep 17 00:00:00 2001 From: Andrew Fiddian-Green Date: Wed, 2 Apr 2025 14:43:17 +0100 Subject: [PATCH 3/7] improvements to javadoc, config, readme, and ui Signed-off-by: Andrew Fiddian-Green --- bundles/org.openhab.binding.tado/README.md | 24 +++++++---- .../tado/internal/TadoBindingConstants.java | 1 + .../tado/internal/config/TadoHomeConfig.java | 2 +- .../internal/handler/TadoHandlerFactory.java | 41 ++++++++++--------- .../internal/handler/TadoHomeHandler.java | 21 ++++++---- .../servlet/TadoAuthenticationServlet.java | 14 +++---- .../resources/OH-INF/i18n/tado.properties | 9 +--- .../resources/OH-INF/thing/thing-types.xml | 8 ++-- 8 files changed, 68 insertions(+), 52 deletions(-) diff --git a/bundles/org.openhab.binding.tado/README.md b/bundles/org.openhab.binding.tado/README.md index 292ad1ee44e6b..38ad179f4f82b 100644 --- a/bundles/org.openhab.binding.tado/README.md +++ b/bundles/org.openhab.binding.tado/README.md @@ -17,13 +17,23 @@ There are two ways to authenticate it as follows: 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. -Note: after March 15th, 2025 online authentication is the tado° preferred method. -It is possible that the `username` and `password` method may cease to work some time after this date. - -| Parameter | Required | Description | -|------------|----------|-----------------------------------------------------------| -| `username` | yes | Username used to log in at [my.tado](https://my.tado.com) | -| `password` | yes | Password of the username | +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. + +| 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. +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. Example `tado.things` diff --git a/bundles/org.openhab.binding.tado/src/main/java/org/openhab/binding/tado/internal/TadoBindingConstants.java b/bundles/org.openhab.binding.tado/src/main/java/org/openhab/binding/tado/internal/TadoBindingConstants.java index a99b35c42dc31..d42b6f14d8047 100644 --- a/bundles/org.openhab.binding.tado/src/main/java/org/openhab/binding/tado/internal/TadoBindingConstants.java +++ b/bundles/org.openhab.binding.tado/src/main/java/org/openhab/binding/tado/internal/TadoBindingConstants.java @@ -138,6 +138,7 @@ public enum OperationMode { // Configuration public static final String CONFIG_ZONE_ID = "id"; public static final String CONFIG_MOBILE_DEVICE_ID = "id"; + public static final String CONFIG_USE_RFC8628 = "useRfc8628"; // Properties public static final String PROPERTY_ZONE_NAME = "name"; diff --git a/bundles/org.openhab.binding.tado/src/main/java/org/openhab/binding/tado/internal/config/TadoHomeConfig.java b/bundles/org.openhab.binding.tado/src/main/java/org/openhab/binding/tado/internal/config/TadoHomeConfig.java index 2a734024c6389..ca147c5998084 100644 --- a/bundles/org.openhab.binding.tado/src/main/java/org/openhab/binding/tado/internal/config/TadoHomeConfig.java +++ b/bundles/org.openhab.binding.tado/src/main/java/org/openhab/binding/tado/internal/config/TadoHomeConfig.java @@ -25,6 +25,6 @@ public class TadoHomeConfig { public @Nullable String username; public @Nullable String password; public @Nullable Boolean useRfc8628; - public @Nullable Boolean rfcWithAccount; + public @Nullable Boolean rfcWithUser; public @Nullable Integer homeId; } diff --git a/bundles/org.openhab.binding.tado/src/main/java/org/openhab/binding/tado/internal/handler/TadoHandlerFactory.java b/bundles/org.openhab.binding.tado/src/main/java/org/openhab/binding/tado/internal/handler/TadoHandlerFactory.java index 5086dcc722303..ba1ccb0b558f2 100644 --- a/bundles/org.openhab.binding.tado/src/main/java/org/openhab/binding/tado/internal/handler/TadoHandlerFactory.java +++ b/bundles/org.openhab.binding.tado/src/main/java/org/openhab/binding/tado/internal/handler/TadoHandlerFactory.java @@ -57,6 +57,7 @@ * handlers. * * @author Dennis Frommknecht - Initial contribution + * @author Andrew Fiddian-Green - OAuth RFC18628 authentication */ @NonNullByDefault @Component(configurationPid = "binding.tado", service = ThingHandlerFactory.class) @@ -148,10 +149,10 @@ protected synchronized void removeHandler(ThingHandler thingHandler) { * If necessary also registers the {@link TadoAuthenticationServlet}. * * @param tadoHomeHandler the subscribing thing handler - * @param account the optional account name (may be null) + * @param user the optional user name (may be null) * @return an {@link OAuthClientService} */ - public OAuthClientService subscribeOAuthClientService(TadoHomeHandler tadoHomeHandler, @Nullable String account) { + public OAuthClientService subscribeOAuthClientService(TadoHomeHandler tadoHomeHandler, @Nullable String user) { if (oAuthClientServiceSubscribers.isEmpty()) { try { httpService.registerServlet(TadoAuthenticationServlet.PATH, httpServlet, null, null); @@ -162,18 +163,18 @@ public OAuthClientService subscribeOAuthClientService(TadoHomeHandler tadoHomeHa oAuthClientServiceSubscribers.add(tadoHomeHandler); - OAuthClientService oAuthClientService = oAuthClientServices.get(getServiceId(account)); + OAuthClientService oAuthClientService = oAuthClientServices.get(getServiceId(user)); if (oAuthClientService == null) { - oAuthClientService = oAuthFactory.getOAuthClientService(getServiceId(account)); + oAuthClientService = oAuthFactory.getOAuthClientService(getServiceId(user)); if (oAuthClientService != null) { - oAuthClientServices.put(getServiceId(account), oAuthClientService); + oAuthClientServices.put(getServiceId(user), oAuthClientService); } } if (oAuthClientService == null) { - oAuthClientService = oAuthFactory.createOAuthClientService(getServiceId(account), OAUTH_TOKEN_URL, + oAuthClientService = oAuthFactory.createOAuthClientService(getServiceId(user), OAUTH_TOKEN_URL, OAUTH_DEVICE_URL, OAUTH_CLIENT_ID, null, OAUTH_SCOPE, false); - oAuthClientServices.put(getServiceId(account), oAuthClientService); + oAuthClientServices.put(getServiceId(user), oAuthClientService); } return oAuthClientService; @@ -195,12 +196,12 @@ public void unsubscribeOAuthClientService(TadoHomeHandler tadoHomeHandler) { /** * Returns a nullable {@link AccessTokenResponse} if the OAuthClientService exists. * - * @param account the optional account name (may be null) + * @param user the optional user name (may be null) * @return a nullable {@link AccessTokenResponse}. * @throws OAuthException on any error */ - public @Nullable AccessTokenResponse getAccessTokenResponse(@Nullable String account) throws OAuthException { - OAuthClientService oAuthClientService = oAuthClientServices.get(getServiceId(account)); + public @Nullable AccessTokenResponse getAccessTokenResponse(@Nullable String user) throws OAuthException { + OAuthClientService oAuthClientService = oAuthClientServices.get(getServiceId(user)); if (oAuthClientService == null) { throw new OAuthException("Missing OAuthClientService"); } @@ -215,12 +216,12 @@ public void unsubscribeOAuthClientService(TadoHomeHandler tadoHomeHandler) { /** * Returns a non null DeviceCodeResponse from the OAuthClientService if it exists. * - * @param account the optional account name (may be null) + * @param user the optional user name (may be null) * @return a {@link DeviceCodeResponseDTO} * @throws OAuthException if it cannot return a non null result */ - public DeviceCodeResponseDTO getDeviceCodeResponse(@Nullable String account) throws OAuthException { - OAuthClientService oAuthClientService = oAuthClientServices.get(getServiceId(account)); + public DeviceCodeResponseDTO getDeviceCodeResponse(@Nullable String user) throws OAuthException { + OAuthClientService oAuthClientService = oAuthClientServices.get(getServiceId(user)); if (oAuthClientService == null) { throw new OAuthException("Missing OAuthClientService"); } @@ -234,18 +235,18 @@ public DeviceCodeResponseDTO getDeviceCodeResponse(@Nullable String account) thr /** * Check if there is an OAuthClientService registered * - * @param account the optional account name (may be null) + * @param user the optional user name (may be null) */ - public boolean hasOAuthClientService(@Nullable String account) { - return oAuthClientServices.containsKey(getServiceId(account)); + public boolean hasOAuthClientService(@Nullable String user) { + return oAuthClientServices.containsKey(getServiceId(user)); } /** - * Build a unique OAuth service id using the (optional) account if present and not blank + * Build a unique OAuth service id using the (optional) user name if present and not blank * - * @param account the optional account name (may be null) + * @param user the optional user name (may be null) */ - private String getServiceId(@Nullable String account) { - return THING_TYPE_HOME.toString() + (account != null && !account.isBlank() ? ":" + account : ""); + private String getServiceId(@Nullable String user) { + return THING_TYPE_HOME.toString() + (user != null && !user.isBlank() ? ":" + user : ""); } } diff --git a/bundles/org.openhab.binding.tado/src/main/java/org/openhab/binding/tado/internal/handler/TadoHomeHandler.java b/bundles/org.openhab.binding.tado/src/main/java/org/openhab/binding/tado/internal/handler/TadoHomeHandler.java index a68a35e223678..e395a8998af83 100644 --- a/bundles/org.openhab.binding.tado/src/main/java/org/openhab/binding/tado/internal/handler/TadoHomeHandler.java +++ b/bundles/org.openhab.binding.tado/src/main/java/org/openhab/binding/tado/internal/handler/TadoHomeHandler.java @@ -39,6 +39,7 @@ 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; @@ -56,6 +57,7 @@ * The {@link TadoHomeHandler} is the bridge of all home-based things. * * @author Dennis Frommknecht - Initial contribution + * @author Andrew Fiddian-Green - OAuth RFC18628 authentication */ @NonNullByDefault public class TadoHomeHandler extends BaseBridgeHandler implements AccessTokenRefreshListener { @@ -99,9 +101,9 @@ public TemperatureUnit getTemperatureUnit() { public void initialize() { configuration = getConfigAs(TadoHomeConfig.class); - String userName = configuration.username; + String username = configuration.username; String password = configuration.password; - boolean v1CredentialsMissing = userName == null || userName.isBlank() || password == null || password.isBlank(); + boolean v1CredentialsMissing = username == null || username.isBlank() || password == null || password.isBlank(); boolean suggestRfc8628 = false; suggestRfc8628 |= Boolean.TRUE.equals(configuration.useRfc8628); @@ -109,18 +111,23 @@ public void initialize() { suggestRfc8628 |= ZonedDateTime.now().isAfter(OAUTH_MANDATORY_FROM_DATE); if (suggestRfc8628) { - String account = Boolean.TRUE.equals(configuration.rfcWithAccount) // - ? userName != null && !userName.isBlank() ? userName : null + String rfcUser = Boolean.TRUE.equals(configuration.rfcWithUser) // + ? username != null && !username.isBlank() ? username : null : null; - OAuthClientService oAuthClientService = tadoHandlerFactory.subscribeOAuthClientService(this, account); + 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_ACCOUNT, account != null ? account : ""); + 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)); + api = new HomeApiFactory().create(Objects.requireNonNull(username), Objects.requireNonNull(password)); logger.trace("initialize() api v1 created"); confPendingText = CONF_PENDING_USER_CREDS; } diff --git a/bundles/org.openhab.binding.tado/src/main/java/org/openhab/binding/tado/internal/servlet/TadoAuthenticationServlet.java b/bundles/org.openhab.binding.tado/src/main/java/org/openhab/binding/tado/internal/servlet/TadoAuthenticationServlet.java index 498b89f3152ad..f322c99ada7a4 100644 --- a/bundles/org.openhab.binding.tado/src/main/java/org/openhab/binding/tado/internal/servlet/TadoAuthenticationServlet.java +++ b/bundles/org.openhab.binding.tado/src/main/java/org/openhab/binding/tado/internal/servlet/TadoAuthenticationServlet.java @@ -43,7 +43,7 @@ public class TadoAuthenticationServlet extends HttpServlet { private static final String HTML_AUTH_ERROR_TEMPLATE = "$REPLACE$"; private static final String HTML_AUTH_START_TEMPLATE = "click to authenticate"; - public static final String PARAM_NAME_ACCOUNT = "account"; + public static final String PARAM_NAME_USER = "user"; private static final String PARAM_NAME_OAUTH = "oauth"; private static final String PARAM_VALUE_START = "start"; @@ -79,7 +79,7 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // if the query string contains "oauth=start" then serve the user authentication page if (PARAM_VALUE_START.equals(request.getParameter(PARAM_NAME_OAUTH)) - && tadoHandlerFactory.hasOAuthClientService(request.getParameter(PARAM_NAME_ACCOUNT))) { + && tadoHandlerFactory.hasOAuthClientService(request.getParameter(PARAM_NAME_USER))) { serveUserAuthenticationPage(request, response); } else { serveStatusPage(request, response); @@ -102,13 +102,13 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response) private void serveStatusPage(HttpServletRequest request, HttpServletResponse response) throws IOException { String dynamicHtml = null; - if (!tadoHandlerFactory.hasOAuthClientService(request.getParameter(PARAM_NAME_ACCOUNT))) { + if (!tadoHandlerFactory.hasOAuthClientService(request.getParameter(PARAM_NAME_USER))) { dynamicHtml = HTML_AUTH_NOT_REQUIRED; } if (dynamicHtml == null) { try { - if (tadoHandlerFactory.getAccessTokenResponse(request.getParameter(PARAM_NAME_ACCOUNT)) != null) { + if (tadoHandlerFactory.getAccessTokenResponse(request.getParameter(PARAM_NAME_USER)) != null) { dynamicHtml = HTML_AUTH_PASSED; } } catch (OAuthException e) { @@ -120,8 +120,8 @@ private void serveStatusPage(HttpServletRequest request, HttpServletResponse res if (dynamicHtml == null) { if (request.getRequestURL() instanceof StringBuffer baseUrl) { String dynamicUrl = baseUrl.append("?").append(PARAM_NAME_OAUTH).append("=").append(PARAM_VALUE_START) - .append("&").append(PARAM_NAME_ACCOUNT).append("=") - .append(request.getParameter(PARAM_NAME_ACCOUNT)).toString(); + .append("&").append(PARAM_NAME_USER).append("=").append(request.getParameter(PARAM_NAME_USER)) + .toString(); dynamicHtml = HTML_AUTH_START_TEMPLATE.replace(REPLACE_TAG, dynamicUrl); } else { dynamicHtml = HTML_AUTH_ERROR_TEMPLATE.replace(REPLACE_TAG, ERROR_BAD_URL); @@ -150,7 +150,7 @@ private void serveUserAuthenticationPage(HttpServletRequest request, HttpServlet try { DeviceCodeResponseDTO deviceCodeResponse = tadoHandlerFactory - .getDeviceCodeResponse(request.getParameter(PARAM_NAME_ACCOUNT)); + .getDeviceCodeResponse(request.getParameter(PARAM_NAME_USER)); String userVerificationUri = deviceCodeResponse.getVerificationUriComplete(); if (userVerificationUri != null && !userVerificationUri.isBlank()) { response.sendRedirect(userVerificationUri); diff --git a/bundles/org.openhab.binding.tado/src/main/resources/OH-INF/i18n/tado.properties b/bundles/org.openhab.binding.tado/src/main/resources/OH-INF/i18n/tado.properties index beb95bf8f22b6..b7d695eab6257 100644 --- a/bundles/org.openhab.binding.tado/src/main/resources/OH-INF/i18n/tado.properties +++ b/bundles/org.openhab.binding.tado/src/main/resources/OH-INF/i18n/tado.properties @@ -22,8 +22,8 @@ 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.rfcWithAccount.label = RFC-8628 With Account -thing-type.config.tado.home.rfcWithAccount.description = Determines if the binding shall include the account in oAuth RFC-8628 authentication +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 @@ -118,11 +118,6 @@ channel-type.tado.verticalSwing.state.option.DOWN = DOWN channel-type.tado.verticalSwing.state.option.ON = ON channel-type.tado.verticalSwing.state.option.OFF = OFF -# thing types config - -thing-type.config.tado.home.withAccount.label = OAuth With Account -thing-type.config.tado.home.withAccount.description = Determines if the binding shall include the account name in the oAuth service id - # tado home thing status messages tado.home.status.oauth = Try authenticating at {0} diff --git a/bundles/org.openhab.binding.tado/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.tado/src/main/resources/OH-INF/thing/thing-types.xml index 50dab43c7cdb6..079dca3ef24ab 100644 --- a/bundles/org.openhab.binding.tado/src/main/resources/OH-INF/thing/thing-types.xml +++ b/bundles/org.openhab.binding.tado/src/main/resources/OH-INF/thing/thing-types.xml @@ -19,9 +19,10 @@ Determines if the binding shall use oAuth RFC-8628 authentication - - - Determines if the binding shall include the account in oAuth RFC-8628 authentication + + + Determines if the user name is included in the oAuth RFC-8628 authentication + true @@ -40,6 +41,7 @@ Selects the Home Id to be used (only needed if there are multiple homes) + true From 95680b951cfbbd1a87531030845fd4f3f9ac57af Mon Sep 17 00:00:00 2001 From: Andrew Fiddian-Green Date: Fri, 4 Apr 2025 13:13:46 +0100 Subject: [PATCH 4/7] re-allow authentication on failure Signed-off-by: Andrew Fiddian-Green --- .../tado/internal/handler/TadoHandlerFactory.java | 9 ++++++--- .../tado/internal/servlet/TadoAuthenticationServlet.java | 6 +++--- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/bundles/org.openhab.binding.tado/src/main/java/org/openhab/binding/tado/internal/handler/TadoHandlerFactory.java b/bundles/org.openhab.binding.tado/src/main/java/org/openhab/binding/tado/internal/handler/TadoHandlerFactory.java index ba1ccb0b558f2..79a86c5f18390 100644 --- a/bundles/org.openhab.binding.tado/src/main/java/org/openhab/binding/tado/internal/handler/TadoHandlerFactory.java +++ b/bundles/org.openhab.binding.tado/src/main/java/org/openhab/binding/tado/internal/handler/TadoHandlerFactory.java @@ -198,9 +198,12 @@ public void unsubscribeOAuthClientService(TadoHomeHandler tadoHomeHandler) { * * @param user the optional user name (may be null) * @return a nullable {@link AccessTokenResponse}. - * @throws OAuthException on any error + * @throws OAuthException + * @throws IOException + * @throws OAuthResponseException */ - public @Nullable AccessTokenResponse getAccessTokenResponse(@Nullable String user) throws OAuthException { + public @Nullable AccessTokenResponse getAccessTokenResponse(@Nullable String user) + throws OAuthException, IOException, OAuthResponseException { OAuthClientService oAuthClientService = oAuthClientServices.get(getServiceId(user)); if (oAuthClientService == null) { throw new OAuthException("Missing OAuthClientService"); @@ -209,7 +212,7 @@ public void unsubscribeOAuthClientService(TadoHomeHandler tadoHomeHandler) { return oAuthClientService.getAccessTokenResponse(); } catch (OAuthException | IOException | OAuthResponseException e) { logger.debug("getAccessTokenResponse() error {}", e.getMessage(), e); - throw new OAuthException("OAuthClientService error" + e.getMessage(), e); + throw e; } } diff --git a/bundles/org.openhab.binding.tado/src/main/java/org/openhab/binding/tado/internal/servlet/TadoAuthenticationServlet.java b/bundles/org.openhab.binding.tado/src/main/java/org/openhab/binding/tado/internal/servlet/TadoAuthenticationServlet.java index f322c99ada7a4..b955b8dd9f1a3 100644 --- a/bundles/org.openhab.binding.tado/src/main/java/org/openhab/binding/tado/internal/servlet/TadoAuthenticationServlet.java +++ b/bundles/org.openhab.binding.tado/src/main/java/org/openhab/binding/tado/internal/servlet/TadoAuthenticationServlet.java @@ -24,6 +24,7 @@ import org.openhab.binding.tado.internal.handler.TadoHandlerFactory; import org.openhab.core.auth.client.oauth2.DeviceCodeResponseDTO; import org.openhab.core.auth.client.oauth2.OAuthException; +import org.openhab.core.auth.client.oauth2.OAuthResponseException; /** * The {@link TadoAuthenticationServlet} manages the authorization with the Tado API. @@ -111,9 +112,8 @@ private void serveStatusPage(HttpServletRequest request, HttpServletResponse res if (tadoHandlerFactory.getAccessTokenResponse(request.getParameter(PARAM_NAME_USER)) != null) { dynamicHtml = HTML_AUTH_PASSED; } - } catch (OAuthException e) { - dynamicHtml = HTML_AUTH_ERROR_TEMPLATE.replace(REPLACE_TAG, - e.getMessage() instanceof String exception ? exception : e.getClass().getName()); + } catch (OAuthException | OAuthResponseException e) { + // error already logged => fall through } } From ab666fd458aff617504ee73d8bb09732735ced98 Mon Sep 17 00:00:00 2001 From: Andrew Fiddian-Green Date: Fri, 4 Apr 2025 15:22:53 +0100 Subject: [PATCH 5/7] break threadlock Signed-off-by: Andrew Fiddian-Green --- .../openhab/binding/tado/internal/handler/TadoHomeHandler.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bundles/org.openhab.binding.tado/src/main/java/org/openhab/binding/tado/internal/handler/TadoHomeHandler.java b/bundles/org.openhab.binding.tado/src/main/java/org/openhab/binding/tado/internal/handler/TadoHomeHandler.java index e395a8998af83..dfc3122080374 100644 --- a/bundles/org.openhab.binding.tado/src/main/java/org/openhab/binding/tado/internal/handler/TadoHomeHandler.java +++ b/bundles/org.openhab.binding.tado/src/main/java/org/openhab/binding/tado/internal/handler/TadoHomeHandler.java @@ -271,6 +271,6 @@ public TadoBatteryChecker getBatteryChecker() { @Override public void onAccessTokenResponse(AccessTokenResponse atr) { - initializeBridgeStatusAndPropertiesIfOffline(); + scheduler.submit(() -> initializeBridgeStatusAndPropertiesIfOffline()); } } From 9f4d367efc37a471d1527f8572b291f9e7efcffd Mon Sep 17 00:00:00 2001 From: Andrew Fiddian-Green Date: Tue, 8 Apr 2025 12:43:40 +0100 Subject: [PATCH 6/7] remove scheduler call Signed-off-by: Andrew Fiddian-Green --- .../openhab/binding/tado/internal/handler/TadoHomeHandler.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bundles/org.openhab.binding.tado/src/main/java/org/openhab/binding/tado/internal/handler/TadoHomeHandler.java b/bundles/org.openhab.binding.tado/src/main/java/org/openhab/binding/tado/internal/handler/TadoHomeHandler.java index dfc3122080374..e395a8998af83 100644 --- a/bundles/org.openhab.binding.tado/src/main/java/org/openhab/binding/tado/internal/handler/TadoHomeHandler.java +++ b/bundles/org.openhab.binding.tado/src/main/java/org/openhab/binding/tado/internal/handler/TadoHomeHandler.java @@ -271,6 +271,6 @@ public TadoBatteryChecker getBatteryChecker() { @Override public void onAccessTokenResponse(AccessTokenResponse atr) { - scheduler.submit(() -> initializeBridgeStatusAndPropertiesIfOffline()); + initializeBridgeStatusAndPropertiesIfOffline(); } } From 6db7d297cd900619fdaa65238f69f1318996fb40 Mon Sep 17 00:00:00 2001 From: Andrew Fiddian-Green Date: Fri, 16 May 2025 10:55:44 +0100 Subject: [PATCH 7/7] convert line endings from CRLF back to LF Signed-off-by: Andrew Fiddian-Green --- .../resources/OH-INF/i18n/tado.properties | 264 +++++++++--------- 1 file changed, 132 insertions(+), 132 deletions(-) diff --git a/bundles/org.openhab.binding.tado/src/main/resources/OH-INF/i18n/tado.properties b/bundles/org.openhab.binding.tado/src/main/resources/OH-INF/i18n/tado.properties index d7a57411c32a7..b3cd5183319d3 100644 --- a/bundles/org.openhab.binding.tado/src/main/resources/OH-INF/i18n/tado.properties +++ b/bundles/org.openhab.binding.tado/src/main/resources/OH-INF/i18n/tado.properties @@ -1,132 +1,132 @@ -# add-on - -addon.tado.name = tado° Binding -addon.tado.description = Binding for tado° devices - -# thing types - -thing-type.tado.home.label = Tado Home -thing-type.tado.home.description = The user's tado home -thing-type.tado.mobiledevice.label = Mobile Device -thing-type.tado.mobiledevice.description = Mobile device of a home -thing-type.tado.zone.label = Zone -thing-type.tado.zone.description = A zone of a home -thing-type.tado.zone.channel.batteryLowAlarm.label = Battery Low Alarm -thing-type.tado.zone.channel.batteryLowAlarm.description = ON if one or more devices in the zone have a low battery -thing-type.tado.zone.channel.humidity.label = Humidity -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.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.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 -thing-type.config.tado.mobiledevice.refreshInterval.description = Refresh interval of location state -thing-type.config.tado.zone.fallbackTimerDuration.label = Fallback Timer Duration -thing-type.config.tado.zone.fallbackTimerDuration.description = Timer duration used if no other duration can be determined -thing-type.config.tado.zone.hvacChangeDebounce.label = HVAC Change Debounce Delay -thing-type.config.tado.zone.hvacChangeDebounce.description = Duration in seconds to combine multiple HVAC changes into one. -thing-type.config.tado.zone.id.label = Zone Id -thing-type.config.tado.zone.id.description = Id of the zone -thing-type.config.tado.zone.refreshInterval.label = Refresh Interval -thing-type.config.tado.zone.refreshInterval.description = Refresh interval of home data - -# channel types - -channel-type.tado.acPower.label = Air-conditioning Power -channel-type.tado.acPower.description = Current power state of the air-conditioning -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 -channel-type.tado.currentTemperature.description = Current temperature -channel-type.tado.fanLevel.label = Fan Speed -channel-type.tado.fanLevel.description = AC fan level (only if supported by AC) -channel-type.tado.fanLevel.state.option.SILENT = SILENT -channel-type.tado.fanLevel.state.option.LEVEL1 = LEVEL1 -channel-type.tado.fanLevel.state.option.LEVEL2 = LEVEL2 -channel-type.tado.fanLevel.state.option.LEVEL3 = LEVEL3 -channel-type.tado.fanLevel.state.option.LEVEL4 = LEVEL4 -channel-type.tado.fanLevel.state.option.LEVEL5 = LEVEL5 -channel-type.tado.fanLevel.state.option.AUTO = AUTO -channel-type.tado.fanspeed.label = Fan Speed -channel-type.tado.fanspeed.description = AC fan speed (only if supported by AC) -channel-type.tado.fanspeed.state.option.LOW = Low -channel-type.tado.fanspeed.state.option.MIDDLE = Middle -channel-type.tado.fanspeed.state.option.HIGH = High -channel-type.tado.fanspeed.state.option.AUTO = Auto -channel-type.tado.geofencingEnabled.label = Geofencing Enabled -channel-type.tado.geofencingEnabled.description = Selects if automatic geofencing is enabled or disabled -channel-type.tado.heatingPower.label = Heating Power -channel-type.tado.heatingPower.description = Current heating power -channel-type.tado.homePresence.label = At Home -channel-type.tado.homePresence.description = ON if at home, OFF if away -channel-type.tado.horizontalSwing.label = Horizontal Swing -channel-type.tado.horizontalSwing.description = State of AC horizontal swing (only if supported by AC) -channel-type.tado.horizontalSwing.state.option.AUTO = AUTO -channel-type.tado.horizontalSwing.state.option.LEFT = LEFT -channel-type.tado.horizontalSwing.state.option.MID_LEFT = MID-LEFT -channel-type.tado.horizontalSwing.state.option.MID = MID -channel-type.tado.horizontalSwing.state.option.MID_RIGHT = MID-RIGHT -channel-type.tado.horizontalSwing.state.option.RIGHT = RIGHT -channel-type.tado.horizontalSwing.state.option.ON = ON -channel-type.tado.horizontalSwing.state.option.OFF = OFF -channel-type.tado.hvacMode.label = HVAC Mode -channel-type.tado.hvacMode.description = Mode of the device (OFF, HEAT, COOL, DRY, FAN, AUTO - if supported) -channel-type.tado.hvacMode.state.option.OFF = Off -channel-type.tado.hvacMode.state.option.HEAT = Heat -channel-type.tado.hvacMode.state.option.COOL = Cool -channel-type.tado.hvacMode.state.option.DRY = Dry -channel-type.tado.hvacMode.state.option.FAN = Fan -channel-type.tado.hvacMode.state.option.AUTO = Auto -channel-type.tado.light.label = Light -channel-type.tado.light.description = State of control panel light (only if supported by AC) -channel-type.tado.openWindowDetected.label = Open Window Detected -channel-type.tado.openWindowDetected.description = Indicates if an open window has been detected -channel-type.tado.openWindowRemainingTime.label = Override Remaining Time -channel-type.tado.openWindowRemainingTime.description = The remaining Open Window heating/cooling Override time in the Zone -channel-type.tado.operationMode.label = Zone Operation Mode -channel-type.tado.operationMode.description = Active operation mode (schedule, manual, timer or until next change) -channel-type.tado.operationMode.state.option.SCHEDULE = Schedule -channel-type.tado.operationMode.state.option.MANUAL = Manual -channel-type.tado.operationMode.state.option.UNTIL_CHANGE = Until change -channel-type.tado.operationMode.state.option.TIMER = Timer -channel-type.tado.overlayExpiry.label = Overlay End Time -channel-type.tado.overlayExpiry.description = Time until when the overlay is active. Null if no overlay is set or overlay type is manual. -channel-type.tado.overlayExpiry.state.pattern = %1$tF %1$tR -channel-type.tado.swing.label = Swing -channel-type.tado.swing.description = State of AC swing (only if supported by AC) -channel-type.tado.targetTemperature.label = Target Temperature -channel-type.tado.targetTemperature.description = Thermostat temperature setpoint -channel-type.tado.timerDuration.label = Timer Duration -channel-type.tado.timerDuration.description = Total duration of a timer -channel-type.tado.verticalSwing.label = Vertical Swing -channel-type.tado.verticalSwing.description = State of AC vertical swing (only if supported by AC) -channel-type.tado.verticalSwing.state.option.AUTO = AUTO -channel-type.tado.verticalSwing.state.option.UP = UP -channel-type.tado.verticalSwing.state.option.MID_UP = MID-UP -channel-type.tado.verticalSwing.state.option.MID = MID -channel-type.tado.verticalSwing.state.option.MID_DOWN = MID-DOWN -channel-type.tado.verticalSwing.state.option.DOWN = DOWN -channel-type.tado.verticalSwing.state.option.ON = ON -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 - -# discovery - -discovery.bridge.label = tado° Internet Bridge ({0}) +# add-on + +addon.tado.name = tado° Binding +addon.tado.description = Binding for tado° devices + +# thing types + +thing-type.tado.home.label = Tado Home +thing-type.tado.home.description = The user's tado home +thing-type.tado.mobiledevice.label = Mobile Device +thing-type.tado.mobiledevice.description = Mobile device of a home +thing-type.tado.zone.label = Zone +thing-type.tado.zone.description = A zone of a home +thing-type.tado.zone.channel.batteryLowAlarm.label = Battery Low Alarm +thing-type.tado.zone.channel.batteryLowAlarm.description = ON if one or more devices in the zone have a low battery +thing-type.tado.zone.channel.humidity.label = Humidity +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.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.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 +thing-type.config.tado.mobiledevice.refreshInterval.description = Refresh interval of location state +thing-type.config.tado.zone.fallbackTimerDuration.label = Fallback Timer Duration +thing-type.config.tado.zone.fallbackTimerDuration.description = Timer duration used if no other duration can be determined +thing-type.config.tado.zone.hvacChangeDebounce.label = HVAC Change Debounce Delay +thing-type.config.tado.zone.hvacChangeDebounce.description = Duration in seconds to combine multiple HVAC changes into one. +thing-type.config.tado.zone.id.label = Zone Id +thing-type.config.tado.zone.id.description = Id of the zone +thing-type.config.tado.zone.refreshInterval.label = Refresh Interval +thing-type.config.tado.zone.refreshInterval.description = Refresh interval of home data + +# channel types + +channel-type.tado.acPower.label = Air-conditioning Power +channel-type.tado.acPower.description = Current power state of the air-conditioning +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 +channel-type.tado.currentTemperature.description = Current temperature +channel-type.tado.fanLevel.label = Fan Speed +channel-type.tado.fanLevel.description = AC fan level (only if supported by AC) +channel-type.tado.fanLevel.state.option.SILENT = SILENT +channel-type.tado.fanLevel.state.option.LEVEL1 = LEVEL1 +channel-type.tado.fanLevel.state.option.LEVEL2 = LEVEL2 +channel-type.tado.fanLevel.state.option.LEVEL3 = LEVEL3 +channel-type.tado.fanLevel.state.option.LEVEL4 = LEVEL4 +channel-type.tado.fanLevel.state.option.LEVEL5 = LEVEL5 +channel-type.tado.fanLevel.state.option.AUTO = AUTO +channel-type.tado.fanspeed.label = Fan Speed +channel-type.tado.fanspeed.description = AC fan speed (only if supported by AC) +channel-type.tado.fanspeed.state.option.LOW = Low +channel-type.tado.fanspeed.state.option.MIDDLE = Middle +channel-type.tado.fanspeed.state.option.HIGH = High +channel-type.tado.fanspeed.state.option.AUTO = Auto +channel-type.tado.geofencingEnabled.label = Geofencing Enabled +channel-type.tado.geofencingEnabled.description = Selects if automatic geofencing is enabled or disabled +channel-type.tado.heatingPower.label = Heating Power +channel-type.tado.heatingPower.description = Current heating power +channel-type.tado.homePresence.label = At Home +channel-type.tado.homePresence.description = ON if at home, OFF if away +channel-type.tado.horizontalSwing.label = Horizontal Swing +channel-type.tado.horizontalSwing.description = State of AC horizontal swing (only if supported by AC) +channel-type.tado.horizontalSwing.state.option.AUTO = AUTO +channel-type.tado.horizontalSwing.state.option.LEFT = LEFT +channel-type.tado.horizontalSwing.state.option.MID_LEFT = MID-LEFT +channel-type.tado.horizontalSwing.state.option.MID = MID +channel-type.tado.horizontalSwing.state.option.MID_RIGHT = MID-RIGHT +channel-type.tado.horizontalSwing.state.option.RIGHT = RIGHT +channel-type.tado.horizontalSwing.state.option.ON = ON +channel-type.tado.horizontalSwing.state.option.OFF = OFF +channel-type.tado.hvacMode.label = HVAC Mode +channel-type.tado.hvacMode.description = Mode of the device (OFF, HEAT, COOL, DRY, FAN, AUTO - if supported) +channel-type.tado.hvacMode.state.option.OFF = Off +channel-type.tado.hvacMode.state.option.HEAT = Heat +channel-type.tado.hvacMode.state.option.COOL = Cool +channel-type.tado.hvacMode.state.option.DRY = Dry +channel-type.tado.hvacMode.state.option.FAN = Fan +channel-type.tado.hvacMode.state.option.AUTO = Auto +channel-type.tado.light.label = Light +channel-type.tado.light.description = State of control panel light (only if supported by AC) +channel-type.tado.openWindowDetected.label = Open Window Detected +channel-type.tado.openWindowDetected.description = Indicates if an open window has been detected +channel-type.tado.openWindowRemainingTime.label = Override Remaining Time +channel-type.tado.openWindowRemainingTime.description = The remaining Open Window heating/cooling Override time in the Zone +channel-type.tado.operationMode.label = Zone Operation Mode +channel-type.tado.operationMode.description = Active operation mode (schedule, manual, timer or until next change) +channel-type.tado.operationMode.state.option.SCHEDULE = Schedule +channel-type.tado.operationMode.state.option.MANUAL = Manual +channel-type.tado.operationMode.state.option.UNTIL_CHANGE = Until change +channel-type.tado.operationMode.state.option.TIMER = Timer +channel-type.tado.overlayExpiry.label = Overlay End Time +channel-type.tado.overlayExpiry.description = Time until when the overlay is active. Null if no overlay is set or overlay type is manual. +channel-type.tado.overlayExpiry.state.pattern = %1$tF %1$tR +channel-type.tado.swing.label = Swing +channel-type.tado.swing.description = State of AC swing (only if supported by AC) +channel-type.tado.targetTemperature.label = Target Temperature +channel-type.tado.targetTemperature.description = Thermostat temperature setpoint +channel-type.tado.timerDuration.label = Timer Duration +channel-type.tado.timerDuration.description = Total duration of a timer +channel-type.tado.verticalSwing.label = Vertical Swing +channel-type.tado.verticalSwing.description = State of AC vertical swing (only if supported by AC) +channel-type.tado.verticalSwing.state.option.AUTO = AUTO +channel-type.tado.verticalSwing.state.option.UP = UP +channel-type.tado.verticalSwing.state.option.MID_UP = MID-UP +channel-type.tado.verticalSwing.state.option.MID = MID +channel-type.tado.verticalSwing.state.option.MID_DOWN = MID-DOWN +channel-type.tado.verticalSwing.state.option.DOWN = DOWN +channel-type.tado.verticalSwing.state.option.ON = ON +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 + +# discovery + +discovery.bridge.label = tado° Internet Bridge ({0})