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 1b2aedcd188f7..10dfbc65e6a47 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,13 +23,6 @@ */ @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"; @@ -89,11 +82,6 @@ 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 = 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 57b4e8bfef21e..d5e762b2e0c90 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 @@ -33,6 +33,7 @@ import org.eclipse.jetty.http.HttpStatus; import org.eclipse.jetty.http.MimeTypes; import org.openhab.binding.huesync.internal.HueSyncConstants.ENDPOINTS; +import org.openhab.binding.huesync.internal.config.HueSyncConfiguration; import org.openhab.binding.huesync.internal.exceptions.HueSyncConnectionException; import org.openhab.core.io.net.http.TlsTrustManagerProvider; import org.osgi.framework.BundleContext; @@ -48,6 +49,7 @@ /** * * @author Patrik Gfeller - Initial Contribution + * @author Patrik Gfeller - Issue #18376, Fix/improve log message and exception handling */ @NonNullByDefault public class HueSyncConnection { @@ -55,10 +57,8 @@ public class HueSyncConnection { .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 - * /api/v1//. + * 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"; private static final String API = "api/v1"; @@ -110,10 +110,10 @@ protected ContentResponse execute() throws InterruptedException, ExecutionExcept protected String registrationId = ""; - public HueSyncConnection(HttpClient httpClient, String host, Integer port) + public HueSyncConnection(HttpClient httpClient, HueSyncConfiguration configuration) throws CertificateException, IOException, URISyntaxException { - this.host = host; - this.port = port; + this.host = configuration.host; + this.port = configuration.port; this.deviceUri = new URI(String.format("https://%s:%s", this.host, this.port)); @@ -123,9 +123,10 @@ public HueSyncConnection(HttpClient httpClient, String host, Integer port) this.tlsProviderService = context.registerService(TlsTrustManagerProvider.class.getName(), trustManagerProvider, null); this.httpClient = httpClient; + this.updateAuthentication(configuration.registrationId, configuration.apiAccessToken); } - public void updateAuthentication(String id, String token) { + public final void updateAuthentication(String id, String token) { this.removeAuthentication(); if (!id.isBlank() && !token.isBlank()) { @@ -139,7 +140,6 @@ public void updateAuthentication(String id, String token) { // #region protected protected @Nullable T executeRequest(HttpMethod method, String endpoint, String payload, @Nullable Class type) throws HueSyncConnectionException { - return this.executeRequest(new Request(method, endpoint, payload), type); } @@ -173,43 +173,63 @@ protected void dispose() { // #region private private @Nullable T executeRequest(Request request, @Nullable Class type) throws HueSyncConnectionException { - String message = "@text/connection.generic-error"; + var message = "@text/connection.generic-error"; try { - ContentResponse response = request.execute(); + var 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 - * unauthenticated, to allow for device identification. + * 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 (response.getStatus()) { - case HttpStatus.OK_200 -> { + case HttpStatus.OK_200: return this.deserialize(response.getContentAsString(), type); - } - case HttpStatus.BAD_REQUEST_400 -> { - logger.debug("registration in progress: no token received yet"); - return null; - } - case HttpStatus.UNAUTHORIZED_401 -> message = "@text/connection.invalid-login"; - case HttpStatus.NOT_FOUND_404 -> message = "@text/connection.generic-error"; } - throw new HueSyncConnectionException(message, new HttpResponseException(message, response)); - } catch (JsonProcessingException | InterruptedException | ExecutionException | TimeoutException e) { - var logMessage = message + " {}"; - this.logger.warn(logMessage, e.toString()); + handleResponseStatus(response.getStatus(), new HttpResponseException(response.getReason(), response)); + } catch (ExecutionException e) { + this.logger.trace("{}: {}", e.getMessage(), message); + + if (e.getCause() instanceof HttpResponseException httpResponseException) { + handleResponseStatus(httpResponseException.getResponse().getStatus(), httpResponseException); + } + + throw new HueSyncConnectionException(message, e); + } catch (HttpResponseException e) { + handleResponseStatus(e.getResponse().getStatus(), e); + } catch (JsonProcessingException | InterruptedException | TimeoutException e) { + this.logger.trace("{}: {}", e.getMessage(), message); throw new HueSyncConnectionException(message, e); } + + throw new HueSyncConnectionException(message); + } + + private void handleResponseStatus(int status, Exception e) throws HueSyncConnectionException { + var message = "@text/connection.generic-error"; + + switch (status) { + case HttpStatus.BAD_REQUEST_400: + case HttpStatus.UNAUTHORIZED_401: + message = "@text/connection.invalid-login"; + break; + case HttpStatus.NOT_FOUND_404: + message = "@text/connection.generic-error"; + break; + } + + this.logger.trace("Status: {}, Message Key: {}", status, message); + + throw new HueSyncConnectionException(message, e); } private @Nullable T deserialize(String json, @Nullable Class type) throws JsonProcessingException { 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 920ce7d5ae517..c9162aa8e20ce 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 @@ -50,6 +50,7 @@ * Handles the connection to a Hue HDMI Sync Box using the official API. * * @author Patrik Gfeller - Initial Contribution + * @author Patrik Gfeller - Issue #18376, Fix/improve log message and exception handling */ @NonNullByDefault public class HueSyncDeviceConnection { @@ -62,11 +63,15 @@ public class HueSyncDeviceConnection { public HueSyncDeviceConnection(HttpClient httpClient, HueSyncConfiguration configuration, HueSyncExceptionHandler exceptionHandler) throws CertificateException, IOException, URISyntaxException { - this.exceptionHandler = exceptionHandler; - this.connection = new HueSyncConnection(httpClient, configuration.host, configuration.port); + try { + this.connection = new HueSyncConnection(httpClient, configuration); - registerCommandHandlers(); + registerCommandHandlers(); + } catch (IOException | URISyntaxException | CertificateException e) { + exceptionHandler.handle(e); + throw e; + } } // #region private @@ -200,9 +205,9 @@ public void dispose() { this.connection.dispose(); } - public void updateConfiguration(HueSyncConfiguration config) { - this.logger.debug("Connection configuration update for device {}:{} - Registration Id [{}]", config.host, - config.port, config.registrationId); + public void updateAuthentication(HueSyncConfiguration config) { + this.logger.debug("Configure authentication for device {}:{} - Registration Id [{}]", config.host, config.port, + config.registrationId); this.connection.updateAuthentication(config.registrationId, config.apiAccessToken); } 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 57132a79275e2..81ccd21d87744 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 @@ -12,12 +12,15 @@ */ package org.openhab.binding.huesync.internal.exceptions; +import java.util.Optional; + import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; /** * * @author Patrik Gfeller - Initial contribution + * @author Patrik Gfeller - Issue #18376, Fix/improve log message and exception handling */ @NonNullByDefault public class HueSyncConnectionException extends HueSyncException { @@ -36,4 +39,16 @@ public HueSyncConnectionException(String message) { public @Nullable Exception getInnerException() { return this.innerException; } + + @Override + public @Nullable String getLocalizedMessage() { + var innerMessage = Optional.ofNullable(this.innerException.getLocalizedMessage()); + var message = super.getLocalizedMessage(); + + if (innerMessage.isPresent()) { + message = message + " (" + innerMessage.get() + ")"; + } + + return message; + } } 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 d1dcc41330ccf..c481f1a7c292b 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 @@ -36,6 +36,7 @@ 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.HueSyncConnectionTask; 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; @@ -52,6 +53,7 @@ import org.openhab.core.thing.ThingStatusDetail; import org.openhab.core.thing.ThingStatusInfo; import org.openhab.core.thing.binding.BaseThingHandler; +import org.openhab.core.thing.binding.builder.ThingStatusInfoBuilder; import org.openhab.core.types.Command; import org.openhab.core.types.State; import org.slf4j.Logger; @@ -62,15 +64,31 @@ * channels. * * @author Patrik Gfeller - Initial contribution + * @author Patrik Gfeller - Issue #18376, Fix/improve log message and exception handling */ @NonNullByDefault public class HueSyncHandler extends BaseThingHandler { + /** + * @author Patrik Gfeller - Initial contribution, Issue #18376 + */ + public static class TASKS { + public static final String CONNECT = "Connect"; + public static final String REGISTER = "Registration"; + public static final String POLL = "Update"; + + public static Map delays = Map.ofEntries(Map.entry(TASKS.CONNECT, 0), + Map.entry(TASKS.REGISTER, 5), Map.entry(TASKS.POLL, 10)); + public static Map intervals = Map.ofEntries(Map.entry(TASKS.CONNECT, 10), + Map.entry(TASKS.REGISTER, 1), Map.entry(TASKS.POLL, 10)); + } + /** * Exception handler implementation * * @author Patrik Gfeller - Initial contribution * @author Patrik Gfeller - Issue #18062, improve connection exception handling. + * @author Patrik Gfeller - Issue #18376, Fix/improve log message and exception handling */ private class ExceptionHandler implements HueSyncExceptionHandler { private final HueSyncHandler handler; @@ -82,35 +100,46 @@ private ExceptionHandler(HueSyncHandler 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(); + String description = exception.getLocalizedMessage(); + + HttpResponseException httpResponseException = null; + + if (exception instanceof HueSyncConnectionException connectionException + && connectionException.getInnerException() instanceof HttpResponseException responseException) { + httpResponseException = responseException; + } + if (exception instanceof HttpResponseException responseException) { + httpResponseException = responseException; + } + + if (httpResponseException != null) { + detail = getThingStatusDetail(httpResponseException); } ThingStatusInfo statusInfo = new ThingStatusInfo(ThingStatus.OFFLINE, detail, description); this.handler.thing.setStatusInfo(statusInfo); + + if (!(detail == ThingStatusDetail.CONFIGURATION_PENDING && tasks.containsKey(TASKS.REGISTER))) { + scheduler.execute(initializeHandler()); + } } - } - private static final String REGISTER = "Registration"; - private static final String POLL = "Update"; + private ThingStatusDetail getThingStatusDetail(HttpResponseException innerException) { + ThingStatusDetail detail; + 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; + } + } + return detail; + } + } private static final String PROPERTY_API_VERSION = "apiVersion"; @@ -127,110 +156,73 @@ public void handle(Exception exception) { public HueSyncHandler(Thing thing, HttpClientFactory httpClientFactory) { super(thing); - this.updateStatus(ThingStatus.UNKNOWN); - 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() { + private synchronized Runnable initializeHandler() { return () -> { - 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); - } + this.stopTasks(); + this.startTasks(); }; } - 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); - - 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 synchronized void startTasks(HueSyncDeviceConnection connection) { - this.stopTasks(); + private synchronized void startTasks() { + String taskId = TASKS.POLL; - connection.updateConfiguration(this.getConfigAs(HueSyncConfiguration.class)); + if (this.connection.isEmpty()) { + taskId = TASKS.CONNECT; + } else if (!this.connection.get().isRegistered()) { + taskId = TASKS.REGISTER; + } Runnable task = null; - String id = connection.isRegistered() ? POLL : REGISTER; - this.logger.debug("startTasks - [{}]", id); + long delay = TASKS.delays.get(taskId); + long interval = TASKS.intervals.get(taskId); - long initialDelay = 0; - long interval = 0; + this.logger.trace("startTasks - [{}, delay: {}s, interval: {}s]", taskId, delay, interval); - switch (id) { - case POLL -> { - this.updateStatus(ThingStatus.ONLINE); + switch (taskId) { + case TASKS.CONNECT -> { + task = new HueSyncConnectionTask(this, this.httpClient, instance -> this.handleConnection(instance), + this.exceptionHandler); + break; + } + case TASKS.POLL -> { + ThingStatusInfo statusInfo = ThingStatusInfoBuilder.create(ThingStatus.ONLINE).build(); + this.thing.setStatusInfo(statusInfo); - initialDelay = HueSyncConstants.POLL_INITIAL_DELAY; + interval = this.getHueSyncConfiguration().statusUpdateInterval; - interval = this.getConfigAs(HueSyncConfiguration.class).statusUpdateInterval; - task = new HueSyncUpdateTask(connection, this.deviceInfo.get(), + task = new HueSyncUpdateTask(this.connection.get(), this.deviceInfo.get(), deviceStatus -> this.handleUpdate(deviceStatus), this.exceptionHandler); + break; } - case REGISTER -> { - initialDelay = HueSyncConstants.REGISTRATION_INITIAL_DELAY; - interval = HueSyncConstants.REGISTRATION_INTERVAL; - - this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING, - "@text/thing.config.huesync.box.registration"); - - task = new HueSyncRegistrationTask(connection, this.deviceInfo.get(), - registration -> this.handleRegistration(registration, connection), this.exceptionHandler); + case TASKS.REGISTER -> { + task = new HueSyncRegistrationTask(this.connection.get(), this.deviceInfo.get(), + this.getHueSyncConfiguration(), registration -> this.handleRegistration(registration), + this.exceptionHandler); + break; } } if (task != null) { - logger.debug("Starting task [{}]", id); - this.tasks.put(id, this.executeTask(task, initialDelay, interval)); + logger.info("Starting task [{}]", taskId); + this.tasks.put(taskId, this.executeTask(task, delay, interval)); } } private synchronized void stopTasks() { - logger.debug("Stopping {} task(s): {}", this.tasks.values().size(), String.join(",", this.tasks.keySet())); + logger.info("Stopping {} task(s): {}", this.tasks.values().size(), String.join(",", this.tasks.keySet())); this.tasks.values().forEach(task -> this.stopTask(task)); this.tasks.clear(); - - this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING, - "@text/thing.config.huesync.box.registration"); } private synchronized void stopTask(@Nullable ScheduledFuture task) { @@ -242,33 +234,15 @@ private synchronized void stopTask(@Nullable ScheduledFuture task) { } 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); - } - } + var result = Optional.ofNullable(dto).orElseThrow(); + + HueSyncDeviceDetailed deviceStatus = Optional.ofNullable(result.deviceStatus).orElseThrow(); + HueSyncHdmi hdmiStatus = Optional.ofNullable(result.hdmiStatus).orElseThrow(); + HueSyncExecution execution = Optional.ofNullable(result.execution).orElseThrow(); + + this.updateFirmwareInformation(deviceStatus); + this.updateHdmiInformation(hdmiStatus); + this.updateExecutionInformation(execution); } private void updateHdmiInformation(HueSyncHdmi hdmiStatus) { @@ -312,28 +286,46 @@ private void updateExecutionInformation(HueSyncExecution executionStatus) { this.updateState(HueSyncConstants.CHANNELS.COMMANDS.BRIGHTNESS, new DecimalType(executionStatus.brightness)); } - private void handleRegistration(HueSyncRegistration registration, HueSyncDeviceConnection connection) { - this.stopTasks(); + private void handleConnection(HueSyncDeviceConnection connectionInstance) { + try { + var information = Optional.ofNullable(connectionInstance.getDeviceInfo()); + + this.deviceInfo = Optional.of(this.checkCompatibility(information)); + this.connection = Optional.of(connectionInstance); + scheduler.execute(initializeHandler()); + } catch (Exception e) { + this.exceptionHandler.handle(e); + } + } + + private void handleRegistration(HueSyncRegistration registration) { setProperty(HueSyncConstants.REGISTRATION_ID, registration.registrationId); - Configuration configuration = this.editConfiguration(); + if ((this.getHueSyncConfiguration().apiAccessToken == null ? registration.accessToken != null + : !this.getHueSyncConfiguration().apiAccessToken.equals(registration.accessToken)) + && (this.getHueSyncConfiguration().registrationId == null ? registration.registrationId != null + : !this.getHueSyncConfiguration().registrationId.equals(registration.registrationId))) { + Configuration configuration = this.editConfiguration(); - configuration.put(HueSyncConstants.REGISTRATION_ID, registration.registrationId); - configuration.put(HueSyncConstants.API_TOKEN, registration.accessToken); + configuration.put(HueSyncConstants.REGISTRATION_ID, registration.registrationId); + configuration.put(HueSyncConstants.API_TOKEN, registration.accessToken); - this.updateConfiguration(configuration); + this.updateConfiguration(configuration); + } - this.startTasks(connection); + scheduler.execute(initializeHandler()); } - private void checkCompatibility() throws HueSyncApiException { + private HueSyncDevice checkCompatibility(Optional deviceInfo) throws HueSyncApiException { try { - HueSyncDevice deviceInformation = this.deviceInfo.orElseThrow(); + HueSyncDevice deviceInformation = deviceInfo.orElseThrow(); if (deviceInformation.apiLevel < HueSyncConstants.MINIMAL_API_VERSION) { throw new HueSyncApiException("@text/api.minimal-version"); } + + return deviceInformation; } catch (NoSuchElementException e) { throw new HueSyncApiException("@text/api.communication-problem"); } @@ -360,26 +352,31 @@ private void saveProperty(String key, String value, Map properti this.updateProperties(properties); } + private HueSyncConfiguration getHueSyncConfiguration() { + return this.getConfigAs(HueSyncConfiguration.class); + } // #endregion // #region Override @Override - public void initialize() { + public synchronized void initialize() { try { - this.stopTasks(); - this.updateStatus(ThingStatus.OFFLINE); - - scheduler.execute(initializeConnection()); + scheduler.execute(initializeHandler()); } catch (Exception e) { - this.stopTasks(); this.logger.warn("{}", e.getMessage()); - this.exceptionHandler.handle(e); } } @Override - public void handleCommand(ChannelUID channelUID, Command command) { + protected synchronized Configuration editConfiguration() { + this.logger.debug("Configuration change detected."); + + return new Configuration(this.thing.getConfiguration().getProperties()); + } + + @Override + public synchronized void handleCommand(ChannelUID channelUID, Command command) { 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()); @@ -397,23 +394,21 @@ public void handleCommand(ChannelUID channelUID, Command command) { } @Override - public void dispose() { - synchronized (this) { - super.dispose(); - - 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()); - } + public synchronized void dispose() { + super.dispose(); + + 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()); } } @Override - public void handleRemoval() { + public synchronized void handleRemoval() { super.handleRemoval(); if (this.connection.isPresent()) { diff --git a/bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/handler/tasks/HueSyncConnectionTask.java b/bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/handler/tasks/HueSyncConnectionTask.java new file mode 100644 index 0000000000000..34e5afce85a97 --- /dev/null +++ b/bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/handler/tasks/HueSyncConnectionTask.java @@ -0,0 +1,56 @@ +/* + * 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.handler.tasks; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.security.cert.CertificateException; +import java.util.function.Consumer; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jetty.client.HttpClient; +import org.openhab.binding.huesync.internal.config.HueSyncConfiguration; +import org.openhab.binding.huesync.internal.connection.HueSyncDeviceConnection; +import org.openhab.binding.huesync.internal.handler.HueSyncHandler; +import org.openhab.binding.huesync.internal.types.HueSyncExceptionHandler; + +/** + * @author Patrik Gfeller - Initial contribution, Issue #18376 + */ +@NonNullByDefault +public class HueSyncConnectionTask implements Runnable { + private final Consumer connectedHandler; + private final HueSyncExceptionHandler exceptionHandler; + private final HttpClient httpClient; + private final HueSyncHandler handler; + + public HueSyncConnectionTask(HueSyncHandler handler, HttpClient httpClient, + Consumer connectionHandler, HueSyncExceptionHandler exceptionHandler) { + this.handler = handler; + this.httpClient = httpClient; + this.connectedHandler = connectionHandler; + this.exceptionHandler = exceptionHandler; + } + + @Override + public void run() { + try { + var connection = new HueSyncDeviceConnection(this.httpClient, + this.handler.getThing().getConfiguration().as(HueSyncConfiguration.class), this.exceptionHandler); + + this.connectedHandler.accept(connection); + } catch (IOException | URISyntaxException | CertificateException e) { + this.exceptionHandler.handle(e); + } + } +} 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 a778f6cc98d87..80dd1b4827671 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 @@ -17,7 +17,9 @@ 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.config.HueSyncConfiguration; import org.openhab.binding.huesync.internal.connection.HueSyncDeviceConnection; +import org.openhab.binding.huesync.internal.exceptions.HueSyncTaskException; import org.openhab.binding.huesync.internal.types.HueSyncExceptionHandler; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -26,6 +28,7 @@ * Task to handle device registration. * * @author Patrik Gfeller - Initial contribution + * @author Patrik Gfeller - Issue #18376, Fix/improve log message and exception handling */ @NonNullByDefault public class HueSyncRegistrationTask implements Runnable { @@ -34,35 +37,48 @@ public class HueSyncRegistrationTask implements Runnable { private final HueSyncDeviceConnection connection; private final HueSyncDevice deviceInfo; private final HueSyncExceptionHandler exceptionHandler; - private final Consumer action; + private final Consumer registrationAccepted; + private final HueSyncConfiguration configuration; public HueSyncRegistrationTask(HueSyncDeviceConnection connection, HueSyncDevice deviceInfo, - Consumer action, HueSyncExceptionHandler exceptionHandler) { - + HueSyncConfiguration configuration, Consumer registrationAccepted, + HueSyncExceptionHandler exceptionHandler) { this.exceptionHandler = exceptionHandler; this.connection = connection; this.deviceInfo = deviceInfo; - this.action = action; + this.registrationAccepted = registrationAccepted; + this.configuration = configuration; } @Override public void run() { try { String id = this.deviceInfo.uniqueId; + HueSyncRegistration registration; + + if (this.connection.isRegistered()) { + this.logger.debug("API token for {} already configured", this.deviceInfo.name); + + registration = new HueSyncRegistration(); + registration.registrationId = this.configuration.registrationId; + registration.accessToken = this.configuration.apiAccessToken; - if (this.connection.isRegistered() || id == null) { + this.registrationAccepted.accept(registration); return; } this.logger.debug("Listening for device registration - {} {}:{}", this.deviceInfo.name, this.deviceInfo.deviceType, id); - HueSyncRegistration registration = this.connection.registerDevice(id); + if (id == null) { + throw new HueSyncTaskException("Device information id must not be null"); + } + + registration = this.connection.registerDevice(id); if (registration != null) { this.logger.debug("API token for {} received", this.deviceInfo.name); - - this.action.accept(registration); + this.registrationAccepted.accept(registration); } } 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 e054f6cac5637..bda2bfcf9c96f 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 @@ -40,7 +40,6 @@ public class HueSyncUpdateTask implements Runnable { public HueSyncUpdateTask(HueSyncDeviceConnection connection, HueSyncDevice deviceInfo, Consumer<@Nullable HueSyncUpdateTaskResult> action, HueSyncExceptionHandler exceptionHandler) { - this.exceptionHandler = exceptionHandler; this.connection = connection; this.deviceInfo = deviceInfo; diff --git a/bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/i18n/ResourceHelper.java b/bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/i18n/ResourceHelper.java index a3d036546d6b3..27c0f82eedbf6 100644 --- a/bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/i18n/ResourceHelper.java +++ b/bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/i18n/ResourceHelper.java @@ -12,8 +12,6 @@ */ package org.openhab.binding.huesync.internal.i18n; -import java.util.Locale; - import org.eclipse.jdt.annotation.NonNullByDefault; import org.openhab.core.i18n.TranslationProvider; import org.osgi.framework.Bundle; @@ -24,26 +22,24 @@ /** * * @author Patrik Gfeller - Initial Contribution + * @author Patrik Gfeller - Issue #18376, Fix/improve log message and exception handling */ @NonNullByDefault public class ResourceHelper { - private static final Locale LOCALE = Locale.ENGLISH; private static final BundleContext BUNDLE_CONTEXT = FrameworkUtil.getBundle(ResourceHelper.class) .getBundleContext(); private static final ServiceReference SERVICE_REFERENCE = BUNDLE_CONTEXT .getServiceReference(TranslationProvider.class); private static final Bundle BUNDLE = BUNDLE_CONTEXT.getBundle(); + private static final TranslationProvider TRANSLATION_PROVIDER = BUNDLE_CONTEXT.getService(SERVICE_REFERENCE); public static String getResourceString(String key) { String lookupKey = key.replace("@text/", ""); String missingKey = "Missing Translation: " + key; - String result = (BUNDLE_CONTEXT - .getService(SERVICE_REFERENCE) instanceof TranslationProvider translationProvider) - ? translationProvider.getText(BUNDLE, lookupKey, missingKey, LOCALE) - : missingKey; + var localizedString = TRANSLATION_PROVIDER.getText(BUNDLE, lookupKey, missingKey, null); - return result == null ? missingKey : result; + return localizedString == null ? missingKey : localizedString; } }