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 2bb8db1816133..1b2aedcd188f7 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 @@ */ @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 static class HDMI { 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 6928b19dac348..5e9216b5cb850 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.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.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 void updateAuthentication(String id, String token) { // #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,51 @@ protected void dispose() { // #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; - } - - private @Nullable T deserialize(String json, Class type) { - try { - return OBJECT_MAPPER.readValue(json, type); - } catch (JsonProcessingException | NoClassDefFoundError e) { - this.logger.error("{}", e.getMessage()); - - return null; - } - } - - 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)); + throw new HueSyncConnectionException(message, new HttpResponseException(message, response)); + } catch (JsonProcessingException | InterruptedException | ExecutionException | TimeoutException e) { + throw new HueSyncConnectionException(message, e); } - - 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 50f2e2623bf36..920ce7d5ae517 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.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 @@ private void execute(String key, Command command) { 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 void executeCommand(Channel channel, Command command) { } } - 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 boolean isRegistered() { } 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 335b3bfc7e51c..57132a79275e2 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 @@ @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/factory/HueSyncHandlerFactory.java b/bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/factory/HueSyncHandlerFactory.java index 7606b1a51f3ba..aef88b118b392 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.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 @@ @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 boolean supportsThingType(ThingTypeUID thingTypeUID) { 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 95fa2a9009acd..2db7acc33161d 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 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.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.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,129 @@ */ @NonNullByDefault public class HueSyncHandler extends BaseThingHandler { + + /** + * Exception handler implementation + * + * @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.updateStatus(ThingStatus.UNKNOWN); + + this.exceptionHandler = new ExceptionHandler(this); this.httpClient = httpClientFactory.getCommonHttpClient(); + } - this.connection = new HueSyncDeviceConnection(this.httpClient, this.getConfigAs(HueSyncConfiguration.class)); + // #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); - - setProperty(HueSyncHandler.PROPERTY_API_VERSION, String.format("%d", info.apiLevel)); - - try { - this.checkCompatibility(); - } catch (HueSyncApiException e) { - this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR); - } finally { - this.startTasks(); - } - }); + try { + var connectionInstance = new HueSyncDeviceConnection(this.httpClient, + this.getConfigAs(HueSyncConfiguration.class), this.exceptionHandler); + + this.connection = Optional.of(connectionInstance); + this.deviceInfo = Optional.ofNullable(connectionInstance.getDeviceInfo()); + + 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 +196,13 @@ private void startTasks() { 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 +211,8 @@ private void startTasks() { 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 +222,7 @@ private void startTasks() { } } - 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 +232,42 @@ private void stopTasks() { "@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 +311,7 @@ private void updateExecutionInformation(HueSyncExecution executionStatus) { 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 +323,7 @@ private void handleRegistration(HueSyncRegistration registration) { this.updateConfiguration(configuration); - this.startTasks(); + this.startTasks(connection); } private void checkCompatibility() throws HueSyncApiException { @@ -291,25 +362,25 @@ private void saveProperty(String key, String value, Map properti // #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 +392,22 @@ public void handleCommand(ChannelUID channelUID, Command command) { 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 +415,9 @@ public void dispose() { 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 41517b74db4ef..a778f6cc98d87 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.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 void run() { 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 3554c64ee0235..b26a0cff04793 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.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 HueSyncUpdateTask(HueSyncDeviceConnection connection, HueSyncDevice devic @Override public void run() { + HueSyncUpdateTaskResult updateInfo = new HueSyncUpdateTaskResult(); + try { this.logger.debug("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(); + } catch (Exception e) { + this.logger.warn("{}", e.getMessage()); + this.exceptionHandler.handle(e); + } finally { this.action.accept(updateInfo); - } catch (Exception e) { - this.logger.debug("{}", e.getMessage()); - this.action.accept(null); } } } 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 0000000000000..23e3732fdb5fa --- /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 - Issue #18062, improve connection exception handling. + */ +@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 8b4a0f04aabf1..4acd7d0af8569 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 connection.invalid-login = Invalid or missing credentials +connection.generic-error = Unable to communicate the device. +connection.server-error = Device was not able to process the request. # registration