[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
parent
51df8f302f
commit
a41c6d4b4f
|
@ -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";
|
||||
}
|
||||
|
|
|
@ -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 = "";
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
|
@ -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);
|
||||
}
|
|
@ -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
|
||||
|
||||
|
|
Loading…
Reference in New Issue