From a41c6d4b4f19e20c13bd1e0d651409cea8aebe03 Mon Sep 17 00:00:00 2001 From: Patrik Gfeller Date: Tue, 18 Feb 2025 13:18:57 +0100 Subject: [PATCH] [huesync] Fix lost api-token when device goes offline (#18100) * fix(18062): [huesync] Configuration (API Token) lost if device goes offline Signed-off-by: Patrik Gfeller --- .../huesync/internal/HueSyncConstants.java | 12 +- .../connection/HueSyncConnection.java | 172 ++++++------- .../connection/HueSyncDeviceConnection.java | 30 ++- .../HueSyncConnectionException.java | 11 + .../internal/exceptions/HueSyncException.java | 4 +- .../factory/HueSyncHandlerFactory.java | 14 +- .../internal/handler/HueSyncHandler.java | 234 ++++++++++++------ .../tasks/HueSyncRegistrationTask.java | 11 +- .../handler/tasks/HueSyncUpdateTask.java | 25 +- ...SyncLocalizer.java => ResourceHelper.java} | 4 +- .../types/HueSyncExceptionHandler.java | 26 ++ .../resources/OH-INF/i18n/huesync.properties | 8 +- 12 files changed, 329 insertions(+), 222 deletions(-) rename bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/i18n/{HueSyncLocalizer.java => ResourceHelper.java} (95%) create mode 100644 bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/types/HueSyncExceptionHandler.java diff --git a/bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/HueSyncConstants.java b/bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/HueSyncConstants.java index 2bb8db18161..1b2aedcd188 100644 --- a/bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/HueSyncConstants.java +++ b/bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/HueSyncConstants.java @@ -23,6 +23,14 @@ import org.openhab.core.thing.ThingTypeUID; */ @NonNullByDefault public class HueSyncConstants { + public static class EXCEPTION_TYPES { + public static class CONNECTION { + public static final String UNAUTHORIZED_401 = "invalidLogin"; + public static final String NOT_FOUND_404 = "notFound"; + public static final String INTERNAL_SERVER_ERROR_500 = "deviceError"; + } + } + public static class ENDPOINTS { public static final String DEVICE = "device"; public static final String REGISTRATIONS = "registrations"; @@ -81,9 +89,11 @@ public class HueSyncConstants { public static final String PARAMETER_HOST = "host"; public static final String PARAMETER_PORT = "port"; - public static final Integer REGISTRATION_INITIAL_DELAY = 3; + public static final Integer REGISTRATION_INITIAL_DELAY = 5; public static final Integer REGISTRATION_INTERVAL = 1; + public static final Integer POLL_INITIAL_DELAY = 10; + public static final String REGISTRATION_ID = "registrationId"; public static final String API_TOKEN = "apiAccessToken"; } diff --git a/bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/connection/HueSyncConnection.java b/bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/connection/HueSyncConnection.java index 6928b19dac3..57b4e8bfef2 100644 --- a/bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/connection/HueSyncConnection.java +++ b/bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/connection/HueSyncConnection.java @@ -18,6 +18,7 @@ import java.net.URISyntaxException; import java.security.cert.CertificateException; import java.util.Optional; import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import org.eclipse.jdt.annotation.NonNullByDefault; @@ -26,8 +27,6 @@ import org.eclipse.jetty.client.HttpClient; import org.eclipse.jetty.client.HttpResponseException; import org.eclipse.jetty.client.api.AuthenticationStore; import org.eclipse.jetty.client.api.ContentResponse; -import org.eclipse.jetty.client.api.Request; -import org.eclipse.jetty.client.api.Response; import org.eclipse.jetty.client.util.StringContentProvider; import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.HttpMethod; @@ -55,8 +54,10 @@ public class HueSyncConnection { public static final ObjectMapper OBJECT_MAPPER = new ObjectMapper() .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); /** - * Request format: The Sync Box API can be accessed locally via HTTPS on root level (port 443, - * /api/v1), resource level /api/v1/ and in some cases sub-resource level + * Request format: The Sync Box API can be accessed locally via HTTPS on root + * level (port 443, + * /api/v1), resource level /api/v1/ and in some cases sub-resource + * level * /api/v1//. */ private static final String REQUEST_FORMAT = "https://%s:%s/%s/%s"; @@ -72,6 +73,41 @@ public class HueSyncConnection { private Optional authentication = Optional.empty(); + private class Request { + + private final String endpoint; + + private HttpMethod method = HttpMethod.GET; + private String payload = ""; + + private Request(HttpMethod httpMethod, String endpoint, String payload) { + this.method = httpMethod; + this.endpoint = endpoint; + this.payload = payload; + } + + protected Request(String endpoint) { + this.endpoint = endpoint; + } + + private Request(HttpMethod httpMethod, String endpoint) { + this.method = httpMethod; + this.endpoint = endpoint; + } + + protected ContentResponse execute() throws InterruptedException, ExecutionException, TimeoutException { + String uri = String.format(REQUEST_FORMAT, host, port, API, endpoint); + + var request = httpClient.newRequest(uri).method(method).timeout(1, TimeUnit.SECONDS); + if (!payload.isBlank()) { + request.header(HttpHeader.CONTENT_TYPE, MimeTypes.Type.APPLICATION_JSON_UTF_8.toString()) + .content(new StringContentProvider(payload)); + } + + return request.send(); + } + } + protected String registrationId = ""; public HueSyncConnection(HttpClient httpClient, String host, Integer port) @@ -102,46 +138,30 @@ public class HueSyncConnection { // #region protected protected @Nullable T executeRequest(HttpMethod method, String endpoint, String payload, - @Nullable Class type) { - try { - return this.processedResponse(this.executeRequest(method, endpoint, payload), type); - } catch (ExecutionException e) { - this.handleExecutionException(e); - } catch (InterruptedException | TimeoutException e) { - this.logger.warn("{}", e.getMessage()); - } + @Nullable Class type) throws HueSyncConnectionException { - return null; + return this.executeRequest(new Request(method, endpoint, payload), type); } - protected @Nullable T executeGetRequest(String endpoint, Class type) { - try { - return this.processedResponse(this.executeGetRequest(endpoint), type); - } catch (ExecutionException e) { - this.handleExecutionException(e); - } catch (InterruptedException | TimeoutException e) { - this.logger.warn("{}", e.getMessage()); - } + protected @Nullable T executeRequest(HttpMethod httpMethod, String endpoint, @Nullable Class type) + throws HueSyncConnectionException { + return this.executeRequest(new Request(httpMethod, endpoint), type); + } - return null; + protected @Nullable T executeGetRequest(String endpoint, Class type) throws HueSyncConnectionException { + return this.executeRequest(new Request(endpoint), type); } protected boolean isRegistered() { return this.authentication.isPresent(); } - protected void unregisterDevice() { + protected void unregisterDevice() throws HueSyncConnectionException { if (this.isRegistered()) { - try { - String endpoint = ENDPOINTS.REGISTRATIONS + "/" + this.registrationId; - ContentResponse response = this.executeRequest(HttpMethod.DELETE, endpoint); + String endpoint = ENDPOINTS.REGISTRATIONS + "/" + this.registrationId; - if (response.getStatus() == HttpStatus.OK_200) { - this.removeAuthentication(); - } - } catch (InterruptedException | TimeoutException | ExecutionException e) { - this.logger.warn("{}", e.getMessage()); - } + this.executeRequest(HttpMethod.DELETE, endpoint, null); + this.removeAuthentication(); } } @@ -151,93 +171,55 @@ public class HueSyncConnection { // #endregion // #region private - private @Nullable T processedResponse(Response response, @Nullable Class type) { - int status = response.getStatus(); + + private @Nullable T executeRequest(Request request, @Nullable Class type) throws HueSyncConnectionException { + String message = "@text/connection.generic-error"; + try { + ContentResponse response = request.execute(); + /* * 400 Invalid State: Registration in progress * - * 401 Authentication failed: If credentials are missing or invalid, errors out. If - * credentials are missing, continues on to GET only the Configuration state when + * 401 Authentication failed: If credentials are missing or invalid, errors out. + * If + * credentials are missing, continues on to GET only the Configuration state + * when * unauthenticated, to allow for device identification. * * 404 Invalid URI Path: Accessing URI path which is not supported * * 500 Internal: Internal errors like out of memory */ - switch (status) { + switch (response.getStatus()) { case HttpStatus.OK_200 -> { - return (type != null && (response instanceof ContentResponse)) - ? this.deserialize(((ContentResponse) response).getContentAsString(), type) - : null; + return this.deserialize(response.getContentAsString(), type); } - case HttpStatus.BAD_REQUEST_400 -> this.logger.debug("registration in progress: no token received yet"); - case HttpStatus.UNAUTHORIZED_401 -> { - this.authentication = Optional.empty(); - throw new HueSyncConnectionException("@text/connection.invalid-login"); + case HttpStatus.BAD_REQUEST_400 -> { + logger.debug("registration in progress: no token received yet"); + return null; } - case HttpStatus.NOT_FOUND_404 -> this.logger.warn("invalid device URI or API endpoint"); - case HttpStatus.INTERNAL_SERVER_ERROR_500 -> this.logger.warn("hue sync box server problem"); - default -> this.logger.warn("unexpected HTTP status: {}", status); + case HttpStatus.UNAUTHORIZED_401 -> message = "@text/connection.invalid-login"; + case HttpStatus.NOT_FOUND_404 -> message = "@text/connection.generic-error"; } - } catch (HueSyncConnectionException e) { - this.logger.warn("{}", e.getMessage()); - } - return null; - } + throw new HueSyncConnectionException(message, new HttpResponseException(message, response)); + } catch (JsonProcessingException | InterruptedException | ExecutionException | TimeoutException e) { - private @Nullable T deserialize(String json, Class type) { - try { - return OBJECT_MAPPER.readValue(json, type); - } catch (JsonProcessingException | NoClassDefFoundError e) { - this.logger.error("{}", e.getMessage()); + var logMessage = message + " {}"; + this.logger.warn(logMessage, e.toString()); - return null; + throw new HueSyncConnectionException(message, e); } } - private ContentResponse executeRequest(HttpMethod method, String endpoint) - throws InterruptedException, TimeoutException, ExecutionException { - return this.executeRequest(method, endpoint, ""); - } - - private ContentResponse executeGetRequest(String endpoint) - throws InterruptedException, ExecutionException, TimeoutException { - String uri = String.format(REQUEST_FORMAT, this.host, this.port, API, endpoint); - - return httpClient.GET(uri); - } - - private ContentResponse executeRequest(HttpMethod method, String endpoint, String payload) - throws InterruptedException, TimeoutException, ExecutionException { - String uri = String.format(REQUEST_FORMAT, this.host, this.port, API, endpoint); - - Request request = this.httpClient.newRequest(uri).method(method); - - this.logger.trace("uri: {}", uri); - this.logger.trace("method: {}", method); - this.logger.trace("payload: {}", payload); - - if (!payload.isBlank()) { - request.header(HttpHeader.CONTENT_TYPE, MimeTypes.Type.APPLICATION_JSON_UTF_8.toString()) - .content(new StringContentProvider(payload)); - } - - return request.send(); - } - - private void handleExecutionException(ExecutionException e) { - this.logger.warn("{}", e.getMessage()); - - Throwable cause = e.getCause(); - if (cause != null && cause instanceof HttpResponseException) { - processedResponse(((HttpResponseException) cause).getResponse(), null); - } + private @Nullable T deserialize(String json, @Nullable Class type) throws JsonProcessingException { + return type == null ? null : OBJECT_MAPPER.readValue(json, type); } private void removeAuthentication() { AuthenticationStore store = this.httpClient.getAuthenticationStore(); store.clearAuthenticationResults(); + this.httpClient.setAuthenticationStore(store); this.registrationId = ""; diff --git a/bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/connection/HueSyncDeviceConnection.java b/bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/connection/HueSyncDeviceConnection.java index 50f2e2623bf..920ce7d5ae5 100644 --- a/bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/connection/HueSyncDeviceConnection.java +++ b/bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/connection/HueSyncDeviceConnection.java @@ -34,6 +34,7 @@ import org.openhab.binding.huesync.internal.api.dto.registration.HueSyncRegistra import org.openhab.binding.huesync.internal.api.dto.registration.HueSyncRegistrationRequest; import org.openhab.binding.huesync.internal.config.HueSyncConfiguration; import org.openhab.binding.huesync.internal.exceptions.HueSyncConnectionException; +import org.openhab.binding.huesync.internal.types.HueSyncExceptionHandler; import org.openhab.core.library.types.OnOffType; import org.openhab.core.library.types.QuantityType; import org.openhab.core.library.types.StringType; @@ -55,11 +56,14 @@ public class HueSyncDeviceConnection { private final Logger logger = LoggerFactory.getLogger(HueSyncDeviceConnection.class); private final HueSyncConnection connection; + private final HueSyncExceptionHandler exceptionHandler; private final Map> deviceCommandExecutors = new HashMap<>(); - public HueSyncDeviceConnection(HttpClient httpClient, HueSyncConfiguration configuration) - throws CertificateException, IOException, URISyntaxException { + public HueSyncDeviceConnection(HttpClient httpClient, HueSyncConfiguration configuration, + HueSyncExceptionHandler exceptionHandler) throws CertificateException, IOException, URISyntaxException { + + this.exceptionHandler = exceptionHandler; this.connection = new HueSyncConnection(httpClient, configuration.host, configuration.port); registerCommandHandlers(); @@ -109,7 +113,11 @@ public class HueSyncDeviceConnection { String json = String.format("{ \"%s\": %s }", key, value); - this.connection.executeRequest(HttpMethod.PUT, ENDPOINTS.EXECUTION, json, null); + try { + this.connection.executeRequest(HttpMethod.PUT, ENDPOINTS.EXECUTION, json, null); + } catch (HueSyncConnectionException exception) { + exceptionHandler.handle(exception); + } } // #endregion @@ -131,29 +139,29 @@ public class HueSyncDeviceConnection { } } - public @Nullable HueSyncDevice getDeviceInfo() { + public @Nullable HueSyncDevice getDeviceInfo() throws Exception { return this.connection.executeGetRequest(ENDPOINTS.DEVICE, HueSyncDevice.class); } - public @Nullable HueSyncDeviceDetailed getDetailedDeviceInfo() { + public @Nullable HueSyncDeviceDetailed getDetailedDeviceInfo() throws Exception { return this.connection.isRegistered() ? this.connection.executeRequest(HttpMethod.GET, ENDPOINTS.DEVICE, "", HueSyncDeviceDetailed.class) : null; } - public @Nullable HueSyncHdmi getHdmiInfo() { + public @Nullable HueSyncHdmi getHdmiInfo() throws Exception { return this.connection.isRegistered() ? this.connection.executeRequest(HttpMethod.GET, ENDPOINTS.HDMI, "", HueSyncHdmi.class) : null; } - public @Nullable HueSyncExecution getExecutionInfo() { + public @Nullable HueSyncExecution getExecutionInfo() throws Exception { return this.connection.isRegistered() ? this.connection.executeRequest(HttpMethod.GET, ENDPOINTS.EXECUTION, "", HueSyncExecution.class) : null; } - public @Nullable HueSyncRegistration registerDevice(String id) throws HueSyncConnectionException { + public @Nullable HueSyncRegistration registerDevice(String id) throws Exception { if (!id.isBlank()) { try { HueSyncRegistrationRequest dto = new HueSyncRegistrationRequest(); @@ -181,7 +189,11 @@ public class HueSyncDeviceConnection { } public void unregisterDevice() { - this.connection.unregisterDevice(); + try { + this.connection.unregisterDevice(); + } catch (HueSyncConnectionException e) { + this.logger.warn("{}", e.getMessage()); + } } public void dispose() { diff --git a/bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/exceptions/HueSyncConnectionException.java b/bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/exceptions/HueSyncConnectionException.java index 335b3bfc7e5..57132a79275 100644 --- a/bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/exceptions/HueSyncConnectionException.java +++ b/bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/exceptions/HueSyncConnectionException.java @@ -13,6 +13,7 @@ package org.openhab.binding.huesync.internal.exceptions; import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; /** * @@ -21,8 +22,18 @@ import org.eclipse.jdt.annotation.NonNullByDefault; @NonNullByDefault public class HueSyncConnectionException extends HueSyncException { private static final long serialVersionUID = 0L; + private @Nullable Exception innerException = null; + + public HueSyncConnectionException(String message, Exception exception) { + super(message); + this.innerException = exception; + } public HueSyncConnectionException(String message) { super(message); } + + public @Nullable Exception getInnerException() { + return this.innerException; + } } diff --git a/bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/exceptions/HueSyncException.java b/bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/exceptions/HueSyncException.java index 02aa57c0f83..05aa8757b1d 100644 --- a/bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/exceptions/HueSyncException.java +++ b/bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/exceptions/HueSyncException.java @@ -13,7 +13,7 @@ package org.openhab.binding.huesync.internal.exceptions; import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.binding.huesync.internal.i18n.HueSyncLocalizer; +import org.openhab.binding.huesync.internal.i18n.ResourceHelper; /** * Base class for all HueSyncExceptions @@ -25,6 +25,6 @@ public abstract class HueSyncException extends Exception { private static final long serialVersionUID = 0L; public HueSyncException(String message) { - super(message.startsWith("@text") ? HueSyncLocalizer.getResourceString(message) : message); + super(message.startsWith("@text") ? ResourceHelper.getResourceString(message) : message); } } diff --git a/bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/factory/HueSyncHandlerFactory.java b/bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/factory/HueSyncHandlerFactory.java index 7606b1a51f3..aef88b118b3 100644 --- a/bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/factory/HueSyncHandlerFactory.java +++ b/bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/factory/HueSyncHandlerFactory.java @@ -12,9 +12,6 @@ */ package org.openhab.binding.huesync.internal.factory; -import java.io.IOException; -import java.net.URISyntaxException; -import java.security.cert.CertificateException; import java.util.Collections; import java.util.Set; @@ -31,8 +28,6 @@ import org.openhab.core.thing.binding.ThingHandlerFactory; import org.osgi.service.component.annotations.Activate; import org.osgi.service.component.annotations.Component; import org.osgi.service.component.annotations.Reference; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; /** * The {@link HueSyncHandlerFactory} is responsible for creating things and @@ -44,9 +39,7 @@ import org.slf4j.LoggerFactory; @NonNullByDefault @Component(configurationPid = "binding.huesync", service = ThingHandlerFactory.class) public class HueSyncHandlerFactory extends BaseThingHandlerFactory { - private final HttpClientFactory httpClientFactory; - private final Logger logger = LoggerFactory.getLogger(HueSyncHandlerFactory.class); @Activate public HueSyncHandlerFactory(@Reference final HttpClientFactory httpClientFactory) throws Exception { @@ -66,12 +59,7 @@ public class HueSyncHandlerFactory extends BaseThingHandlerFactory { ThingTypeUID thingTypeUID = thing.getThingTypeUID(); if (HueSyncConstants.THING_TYPE_UID.equals(thingTypeUID)) { - try { - return new HueSyncHandler(thing, this.httpClientFactory); - } catch (IOException | URISyntaxException | CertificateException e) { - this.logger.warn("It was not possible to create a handler for {}: {}", thingTypeUID.getId(), - e.getMessage()); - } + return new HueSyncHandler(thing, this.httpClientFactory); } return null; diff --git a/bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/handler/HueSyncHandler.java b/bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/handler/HueSyncHandler.java index 95fa2a9009a..d1dcc41330c 100644 --- a/bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/handler/HueSyncHandler.java +++ b/bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/handler/HueSyncHandler.java @@ -12,9 +12,6 @@ */ package org.openhab.binding.huesync.internal.handler; -import java.io.IOException; -import java.net.URISyntaxException; -import java.security.cert.CertificateException; import java.util.HashMap; import java.util.Map; import java.util.NoSuchElementException; @@ -25,6 +22,8 @@ import java.util.concurrent.TimeUnit; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.HttpResponseException; +import org.eclipse.jetty.http.HttpStatus; import org.openhab.binding.huesync.internal.HdmiChannels; import org.openhab.binding.huesync.internal.HueSyncConstants; import org.openhab.binding.huesync.internal.api.dto.device.HueSyncDevice; @@ -36,9 +35,11 @@ import org.openhab.binding.huesync.internal.api.dto.registration.HueSyncRegistra import org.openhab.binding.huesync.internal.config.HueSyncConfiguration; import org.openhab.binding.huesync.internal.connection.HueSyncDeviceConnection; import org.openhab.binding.huesync.internal.exceptions.HueSyncApiException; +import org.openhab.binding.huesync.internal.exceptions.HueSyncConnectionException; import org.openhab.binding.huesync.internal.handler.tasks.HueSyncRegistrationTask; import org.openhab.binding.huesync.internal.handler.tasks.HueSyncUpdateTask; import org.openhab.binding.huesync.internal.handler.tasks.HueSyncUpdateTaskResult; +import org.openhab.binding.huesync.internal.types.HueSyncExceptionHandler; import org.openhab.core.config.core.Configuration; import org.openhab.core.io.net.http.HttpClientFactory; import org.openhab.core.library.types.DecimalType; @@ -49,6 +50,7 @@ import org.openhab.core.thing.ChannelUID; import org.openhab.core.thing.Thing; import org.openhab.core.thing.ThingStatus; import org.openhab.core.thing.ThingStatusDetail; +import org.openhab.core.thing.ThingStatusInfo; import org.openhab.core.thing.binding.BaseThingHandler; import org.openhab.core.types.Command; import org.openhab.core.types.State; @@ -63,70 +65,130 @@ import org.slf4j.LoggerFactory; */ @NonNullByDefault public class HueSyncHandler extends BaseThingHandler { + + /** + * Exception handler implementation + * + * @author Patrik Gfeller - Initial contribution + * @author Patrik Gfeller - Issue #18062, improve connection exception handling. + */ + private class ExceptionHandler implements HueSyncExceptionHandler { + private final HueSyncHandler handler; + + private ExceptionHandler(HueSyncHandler handler) { + this.handler = handler; + } + + @Override + public void handle(Exception exception) { + ThingStatusDetail detail = ThingStatusDetail.COMMUNICATION_ERROR; + String description; + + if (exception instanceof HueSyncConnectionException connectionException) { + if (connectionException.getInnerException() instanceof HttpResponseException innerException) { + switch (innerException.getResponse().getStatus()) { + case HttpStatus.BAD_REQUEST_400 -> { + detail = ThingStatusDetail.CONFIGURATION_PENDING; + } + case HttpStatus.UNAUTHORIZED_401 -> { + detail = ThingStatusDetail.CONFIGURATION_ERROR; + } + default -> { + detail = ThingStatusDetail.COMMUNICATION_ERROR; + } + } + } + description = connectionException.getLocalizedMessage(); + } else { + detail = ThingStatusDetail.COMMUNICATION_ERROR; + description = exception.getLocalizedMessage(); + } + + ThingStatusInfo statusInfo = new ThingStatusInfo(ThingStatus.OFFLINE, detail, description); + this.handler.thing.setStatusInfo(statusInfo); + } + } + private static final String REGISTER = "Registration"; private static final String POLL = "Update"; private static final String PROPERTY_API_VERSION = "apiVersion"; + private final ExceptionHandler exceptionHandler; private final Logger logger = LoggerFactory.getLogger(HueSyncHandler.class); Map> tasks = new HashMap<>(); private Optional deviceInfo = Optional.empty(); + private Optional connection = Optional.empty(); - private final HueSyncDeviceConnection connection; private final HttpClient httpClient; - public HueSyncHandler(Thing thing, HttpClientFactory httpClientFactory) - throws CertificateException, IOException, URISyntaxException { + public HueSyncHandler(Thing thing, HttpClientFactory httpClientFactory) { super(thing); - this.httpClient = httpClientFactory.getCommonHttpClient(); + this.updateStatus(ThingStatus.UNKNOWN); - this.connection = new HueSyncDeviceConnection(this.httpClient, this.getConfigAs(HueSyncConfiguration.class)); + this.exceptionHandler = new ExceptionHandler(this); + this.httpClient = httpClientFactory.getCommonHttpClient(); } + // #region override + @Override + protected Configuration editConfiguration() { + this.logger.debug("Configuration change detected."); + + return new Configuration(this.thing.getConfiguration().getProperties()); + } + // #endregion + // #region private private Runnable initializeConnection() { return () -> { - this.deviceInfo = Optional.ofNullable(this.connection.getDeviceInfo()); - this.deviceInfo.ifPresent(info -> { - setProperty(Thing.PROPERTY_SERIAL_NUMBER, info.uniqueId != null ? info.uniqueId : ""); - setProperty(Thing.PROPERTY_MODEL_ID, info.deviceType); - setProperty(Thing.PROPERTY_FIRMWARE_VERSION, info.firmwareVersion); + try { + var connectionInstance = new HueSyncDeviceConnection(this.httpClient, + this.getConfigAs(HueSyncConfiguration.class), this.exceptionHandler); - setProperty(HueSyncHandler.PROPERTY_API_VERSION, String.format("%d", info.apiLevel)); + this.connection = Optional.of(connectionInstance); + this.deviceInfo = Optional.ofNullable(connectionInstance.getDeviceInfo()); - try { - this.checkCompatibility(); - } catch (HueSyncApiException e) { - this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR); - } finally { - this.startTasks(); - } - }); + this.deviceInfo.ifPresent(info -> { + connect(connectionInstance, info); + }); + + } catch (Exception e) { + this.exceptionHandler.handle(e); + } }; } - private void stopTask(@Nullable ScheduledFuture task) { - if (task == null || task.isCancelled() || task.isDone()) { - return; - } + private void connect(HueSyncDeviceConnection connectionInstance, HueSyncDevice info) { + setProperty(Thing.PROPERTY_SERIAL_NUMBER, info.uniqueId != null ? info.uniqueId : ""); + setProperty(Thing.PROPERTY_MODEL_ID, info.deviceType); + setProperty(Thing.PROPERTY_FIRMWARE_VERSION, info.firmwareVersion); - task.cancel(true); + setProperty(HueSyncHandler.PROPERTY_API_VERSION, String.format("%d", info.apiLevel)); + + try { + this.checkCompatibility(); + } catch (HueSyncApiException e) { + this.exceptionHandler.handle(e); + } finally { + this.startTasks(connectionInstance); + } } private @Nullable ScheduledFuture executeTask(Runnable task, long initialDelay, long interval) { return scheduler.scheduleWithFixedDelay(task, initialDelay, interval, TimeUnit.SECONDS); } - private void startTasks() { + private synchronized void startTasks(HueSyncDeviceConnection connection) { this.stopTasks(); - this.connection.updateConfiguration(this.getConfigAs(HueSyncConfiguration.class)); + connection.updateConfiguration(this.getConfigAs(HueSyncConfiguration.class)); Runnable task = null; - String id = this.connection.isRegistered() ? POLL : REGISTER; + String id = connection.isRegistered() ? POLL : REGISTER; this.logger.debug("startTasks - [{}]", id); @@ -135,13 +197,13 @@ public class HueSyncHandler extends BaseThingHandler { switch (id) { case POLL -> { - initialDelay = 0; - interval = this.getConfigAs(HueSyncConfiguration.class).statusUpdateInterval; - this.updateStatus(ThingStatus.ONLINE); - task = new HueSyncUpdateTask(this.connection, this.deviceInfo.get(), - deviceStatus -> this.handleUpdate(deviceStatus)); + initialDelay = HueSyncConstants.POLL_INITIAL_DELAY; + + interval = this.getConfigAs(HueSyncConfiguration.class).statusUpdateInterval; + task = new HueSyncUpdateTask(connection, this.deviceInfo.get(), + deviceStatus -> this.handleUpdate(deviceStatus), this.exceptionHandler); } case REGISTER -> { initialDelay = HueSyncConstants.REGISTRATION_INITIAL_DELAY; @@ -150,8 +212,8 @@ public class HueSyncHandler extends BaseThingHandler { this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING, "@text/thing.config.huesync.box.registration"); - task = new HueSyncRegistrationTask(this.connection, this.deviceInfo.get(), - registration -> this.handleRegistration(registration)); + task = new HueSyncRegistrationTask(connection, this.deviceInfo.get(), + registration -> this.handleRegistration(registration, connection), this.exceptionHandler); } } @@ -161,7 +223,7 @@ public class HueSyncHandler extends BaseThingHandler { } } - private void stopTasks() { + private synchronized void stopTasks() { logger.debug("Stopping {} task(s): {}", this.tasks.values().size(), String.join(",", this.tasks.keySet())); this.tasks.values().forEach(task -> this.stopTask(task)); @@ -171,32 +233,42 @@ public class HueSyncHandler extends BaseThingHandler { "@text/thing.config.huesync.box.registration"); } - private void handleUpdate(@Nullable HueSyncUpdateTaskResult dto) { - try { - HueSyncUpdateTaskResult update = Optional.ofNullable(dto).get(); - - try { - this.updateFirmwareInformation(Optional.ofNullable(update.deviceStatus).get()); - } catch (NoSuchElementException e) { - this.logMissingUpdateInformation("device"); - } - - this.updateHdmiInformation(Optional.ofNullable(update.hdmiStatus).get()); - this.updateExecutionInformation(Optional.ofNullable(update.execution).get()); - } catch (NoSuchElementException e) { - Configuration configuration = this.editConfiguration(); - - configuration.put(HueSyncConstants.REGISTRATION_ID, ""); - configuration.put(HueSyncConstants.API_TOKEN, ""); - - this.updateConfiguration(configuration); - - this.startTasks(); + private synchronized void stopTask(@Nullable ScheduledFuture task) { + if (task == null || task.isCancelled() || task.isDone()) { + return; } + + task.cancel(true); } - private void logMissingUpdateInformation(String api) { - this.logger.warn("Device information - {} status missing", api); + private void handleUpdate(@Nullable HueSyncUpdateTaskResult dto) { + synchronized (this) { + ThingStatus status = this.thing.getStatus(); + + switch (status) { + case ONLINE: + Optional.ofNullable(dto).ifPresent(taskResult -> { + Optional.ofNullable(taskResult.deviceStatus) + .ifPresent(payload -> this.updateFirmwareInformation(payload)); + Optional.ofNullable(taskResult.hdmiStatus) + .ifPresent(payload -> this.updateHdmiInformation(payload)); + Optional.ofNullable(taskResult.execution) + .ifPresent(payload -> this.updateExecutionInformation(payload)); + }); + break; + case OFFLINE: + this.stopTasks(); + + this.connection.ifPresent(connectionInstance -> { + this.deviceInfo.ifPresent(deviceInfoInstance -> { + this.connect(connectionInstance, deviceInfoInstance); + }); + }); + break; + default: + this.logger.debug("Unable to execute update - Status: [{}]", status); + } + } } private void updateHdmiInformation(HueSyncHdmi hdmiStatus) { @@ -240,7 +312,7 @@ public class HueSyncHandler extends BaseThingHandler { this.updateState(HueSyncConstants.CHANNELS.COMMANDS.BRIGHTNESS, new DecimalType(executionStatus.brightness)); } - private void handleRegistration(HueSyncRegistration registration) { + private void handleRegistration(HueSyncRegistration registration, HueSyncDeviceConnection connection) { this.stopTasks(); setProperty(HueSyncConstants.REGISTRATION_ID, registration.registrationId); @@ -252,7 +324,7 @@ public class HueSyncHandler extends BaseThingHandler { this.updateConfiguration(configuration); - this.startTasks(); + this.startTasks(connection); } private void checkCompatibility() throws HueSyncApiException { @@ -291,25 +363,25 @@ public class HueSyncHandler extends BaseThingHandler { // #endregion // #region Override + @Override public void initialize() { try { - updateStatus(ThingStatus.UNKNOWN); - this.stopTasks(); + this.updateStatus(ThingStatus.OFFLINE); scheduler.execute(initializeConnection()); } catch (Exception e) { + this.stopTasks(); this.logger.warn("{}", e.getMessage()); - - this.updateStatus(ThingStatus.OFFLINE); + this.exceptionHandler.handle(e); } } @Override public void handleCommand(ChannelUID channelUID, Command command) { - if (thing.getStatus() != ThingStatus.ONLINE) { - this.logger.warn("Device status: {} - Command {} for chanel {} will be ignored", + if (thing.getStatus() != ThingStatus.ONLINE || this.connection.isEmpty()) { + this.logger.warn("Device status: {} - Command {} for channel {} will be ignored", thing.getStatus().toString(), command.toFullString(), channelUID.toString()); return; } @@ -321,20 +393,22 @@ public class HueSyncHandler extends BaseThingHandler { return; } - this.connection.executeCommand(channel, command); + this.connection.get().executeCommand(channel, command); } @Override public void dispose() { - super.dispose(); + synchronized (this) { + super.dispose(); - try { - this.stopTasks(); - this.connection.dispose(); - } catch (Exception e) { - this.logger.warn("{}", e.getMessage()); - } finally { - this.logger.debug("Thing {} ({}) disposed.", this.thing.getLabel(), this.thing.getUID()); + try { + this.stopTasks(); + this.connection.orElseThrow().dispose(); + } catch (Exception e) { + this.logger.warn("{}", e.getMessage()); + } finally { + this.logger.debug("Thing {} ({}) disposed.", this.thing.getLabel(), this.thing.getUID()); + } } } @@ -342,7 +416,9 @@ public class HueSyncHandler extends BaseThingHandler { public void handleRemoval() { super.handleRemoval(); - this.connection.unregisterDevice(); + if (this.connection.isPresent()) { + this.connection.get().unregisterDevice(); + } } // #endregion diff --git a/bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/handler/tasks/HueSyncRegistrationTask.java b/bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/handler/tasks/HueSyncRegistrationTask.java index 41517b74db4..a778f6cc98d 100644 --- a/bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/handler/tasks/HueSyncRegistrationTask.java +++ b/bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/handler/tasks/HueSyncRegistrationTask.java @@ -18,7 +18,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.openhab.binding.huesync.internal.api.dto.device.HueSyncDevice; import org.openhab.binding.huesync.internal.api.dto.registration.HueSyncRegistration; import org.openhab.binding.huesync.internal.connection.HueSyncDeviceConnection; -import org.openhab.binding.huesync.internal.exceptions.HueSyncConnectionException; +import org.openhab.binding.huesync.internal.types.HueSyncExceptionHandler; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -33,10 +33,13 @@ public class HueSyncRegistrationTask implements Runnable { private final HueSyncDeviceConnection connection; private final HueSyncDevice deviceInfo; + private final HueSyncExceptionHandler exceptionHandler; private final Consumer action; public HueSyncRegistrationTask(HueSyncDeviceConnection connection, HueSyncDevice deviceInfo, - Consumer action) { + Consumer action, HueSyncExceptionHandler exceptionHandler) { + + this.exceptionHandler = exceptionHandler; this.connection = connection; this.deviceInfo = deviceInfo; this.action = action; @@ -61,8 +64,8 @@ public class HueSyncRegistrationTask implements Runnable { this.action.accept(registration); } - } catch (HueSyncConnectionException e) { - this.logger.warn("{}", e.getMessage()); + } catch (Exception e) { + this.exceptionHandler.handle(e); } } } diff --git a/bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/handler/tasks/HueSyncUpdateTask.java b/bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/handler/tasks/HueSyncUpdateTask.java index 3554c64ee02..e054f6cac56 100644 --- a/bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/handler/tasks/HueSyncUpdateTask.java +++ b/bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/handler/tasks/HueSyncUpdateTask.java @@ -18,6 +18,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.huesync.internal.api.dto.device.HueSyncDevice; import org.openhab.binding.huesync.internal.connection.HueSyncDeviceConnection; +import org.openhab.binding.huesync.internal.types.HueSyncExceptionHandler; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -34,10 +35,13 @@ public class HueSyncUpdateTask implements Runnable { private final HueSyncDeviceConnection connection; private final HueSyncDevice deviceInfo; + private final HueSyncExceptionHandler exceptionHandler; private final Consumer<@Nullable HueSyncUpdateTaskResult> action; public HueSyncUpdateTask(HueSyncDeviceConnection connection, HueSyncDevice deviceInfo, - Consumer<@Nullable HueSyncUpdateTaskResult> action) { + Consumer<@Nullable HueSyncUpdateTaskResult> action, HueSyncExceptionHandler exceptionHandler) { + + this.exceptionHandler = exceptionHandler; this.connection = connection; this.deviceInfo = deviceInfo; @@ -46,24 +50,21 @@ public class HueSyncUpdateTask implements Runnable { @Override public void run() { + HueSyncUpdateTaskResult updateInfo = new HueSyncUpdateTaskResult(); + try { - this.logger.debug("Status update query for {} {}:{}", this.deviceInfo.name, this.deviceInfo.deviceType, + this.logger.trace("Status update query for {} {}:{}", this.deviceInfo.name, this.deviceInfo.deviceType, this.deviceInfo.uniqueId); - if (!this.connection.isRegistered()) { - this.action.accept(null); - } - - HueSyncUpdateTaskResult updateInfo = new HueSyncUpdateTaskResult(); - updateInfo.deviceStatus = this.connection.getDetailedDeviceInfo(); updateInfo.hdmiStatus = this.connection.getHdmiInfo(); updateInfo.execution = this.connection.getExecutionInfo(); - - this.action.accept(updateInfo); } catch (Exception e) { - this.logger.debug("{}", e.getMessage()); - this.action.accept(null); + this.logger.warn("{}", e.getMessage()); + + this.exceptionHandler.handle(e); + } finally { + this.action.accept(updateInfo); } } } diff --git a/bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/i18n/HueSyncLocalizer.java b/bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/i18n/ResourceHelper.java similarity index 95% rename from bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/i18n/HueSyncLocalizer.java rename to bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/i18n/ResourceHelper.java index 7ac5ac0a3e9..a3d036546d6 100644 --- a/bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/i18n/HueSyncLocalizer.java +++ b/bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/i18n/ResourceHelper.java @@ -26,9 +26,9 @@ import org.osgi.framework.ServiceReference; * @author Patrik Gfeller - Initial Contribution */ @NonNullByDefault -public class HueSyncLocalizer { +public class ResourceHelper { private static final Locale LOCALE = Locale.ENGLISH; - private static final BundleContext BUNDLE_CONTEXT = FrameworkUtil.getBundle(HueSyncLocalizer.class) + private static final BundleContext BUNDLE_CONTEXT = FrameworkUtil.getBundle(ResourceHelper.class) .getBundleContext(); private static final ServiceReference SERVICE_REFERENCE = BUNDLE_CONTEXT .getServiceReference(TranslationProvider.class); diff --git a/bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/types/HueSyncExceptionHandler.java b/bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/types/HueSyncExceptionHandler.java new file mode 100644 index 00000000000..d3241deb4ab --- /dev/null +++ b/bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/types/HueSyncExceptionHandler.java @@ -0,0 +1,26 @@ +/* + * 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.huesync.internal.types; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Marker interface for exception handler implementations + * + * @author Patrik Gfeller - Initial contribution + */ +@NonNullByDefault +public interface HueSyncExceptionHandler { + + void handle(Exception exception); +} diff --git a/bundles/org.openhab.binding.huesync/src/main/resources/OH-INF/i18n/huesync.properties b/bundles/org.openhab.binding.huesync/src/main/resources/OH-INF/i18n/huesync.properties index 8b4a0f04aab..f2000e1daf8 100644 --- a/bundles/org.openhab.binding.huesync/src/main/resources/OH-INF/i18n/huesync.properties +++ b/bundles/org.openhab.binding.huesync/src/main/resources/OH-INF/i18n/huesync.properties @@ -91,15 +91,13 @@ channel-type.huesync.execution-mode.command.option.music = Music channel-type.huesync.execution-sync-active.label = Synchronization Active channel-type.huesync.execution-sync-active.description =

OFF in case of powersave or passthrough mode, and ON in case of video, game or music mode.

When changed from OFF to ON, it will start syncing in last used mode for current source. When changed from ON to OFF, will set passthrough mode.

-# *** exceptions *** - -exception.generic.connection = "Unable to connect to device." - # api & connection exceptions api.minimal-version = Only devices with API level >= 7 are supported -api.communication-problem = Communication problem with the device +api.communication-problem = Unable to communicate with the device (API) connection.invalid-login = Invalid or missing credentials +connection.generic-error = Unable to communicate with the device (Generic) +connection.server-error = Device was not able to process the request. # registration