[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 <patrik.gfeller@proton.me>
pull/17817/merge
Patrik Gfeller 2025-02-18 13:18:57 +01:00 committed by GitHub
parent 51df8f302f
commit a41c6d4b4f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 329 additions and 222 deletions

View File

@ -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";
}

View File

@ -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/<resource> 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/<resource> and in some cases sub-resource
* level
* /api/v1/<resource>/<sub-resource>.
*/
private static final String REQUEST_FORMAT = "https://%s:%s/%s/%s";
@ -72,6 +73,41 @@ public class HueSyncConnection {
private Optional<HueSyncAuthenticationResult> 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> T executeRequest(HttpMethod method, String endpoint, String payload,
@Nullable Class<T> 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<T> type) throws HueSyncConnectionException {
return null;
return this.executeRequest(new Request(method, endpoint, payload), type);
}
protected @Nullable <T> T executeGetRequest(String endpoint, Class<T> 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> T executeRequest(HttpMethod httpMethod, String endpoint, @Nullable Class<T> type)
throws HueSyncConnectionException {
return this.executeRequest(new Request(httpMethod, endpoint), type);
}
return null;
protected @Nullable <T> T executeGetRequest(String endpoint, Class<T> 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> T processedResponse(Response response, @Nullable Class<T> type) {
int status = response.getStatus();
private @Nullable <T> T executeRequest(Request request, @Nullable Class<T> 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> T deserialize(String json, Class<T> 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> T deserialize(String json, @Nullable Class<T> 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 = "";

View File

@ -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<String, Consumer<Command>> 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() {

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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;

View File

@ -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<String, @Nullable ScheduledFuture<?>> tasks = new HashMap<>();
private Optional<HueSyncDevice> deviceInfo = Optional.empty();
private Optional<HueSyncDeviceConnection> 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

View File

@ -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<HueSyncRegistration> action;
public HueSyncRegistrationTask(HueSyncDeviceConnection connection, HueSyncDevice deviceInfo,
Consumer<HueSyncRegistration> action) {
Consumer<HueSyncRegistration> 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);
}
}
}

View File

@ -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);
}
}
}

View File

@ -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<TranslationProvider> SERVICE_REFERENCE = BUNDLE_CONTEXT
.getServiceReference(TranslationProvider.class);

View File

@ -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);
}

View File

@ -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 = <p> <b>OFF</b> in case of <i>powersave</i> or <i>passthrough</i> mode, and <b>ON</b> in case of <i>video</i>, <i>game</i> or <i>music</i> mode. </p> <p> When changed from <b>OFF</b> to <b>ON</b>, it will start syncing in last used mode for current source. When changed from <b>ON</b> to <b>OFF</b>, will set <i>passthrough</i> mode. </p>
# *** 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