[nanoleaf] Reimplement touch detection based on SSE, stabilize behavior, add swipe support (#11133)
* [nanoleaf] reimplement touch detection based on sse, stabilize behavior * [nanoleaf] add swipe support * [nanoleaf] add / tested full shapes support Signed-off-by: Stefan Höhn <stefan@andreaundstefanhoehn.de>
This commit is contained in:
@@ -42,6 +42,7 @@ public class NanoleafBindingConstants {
|
||||
|
||||
// Panel configuration settings
|
||||
public static final String CONFIG_PANEL_ID = "id";
|
||||
public static final String CONTROLLER_PANEL_ID = "-1";
|
||||
|
||||
// List of controller channels
|
||||
public static final String CHANNEL_COLOR = "color";
|
||||
@@ -52,6 +53,11 @@ public class NanoleafBindingConstants {
|
||||
public static final String CHANNEL_RHYTHM_STATE = "rhythmState";
|
||||
public static final String CHANNEL_RHYTHM_ACTIVE = "rhythmActive";
|
||||
public static final String CHANNEL_RHYTHM_MODE = "rhythmMode";
|
||||
public static final String CHANNEL_SWIPE = "swipe";
|
||||
public static final String CHANNEL_SWIPE_EVENT_UP = "UP";
|
||||
public static final String CHANNEL_SWIPE_EVENT_DOWN = "DOWN";
|
||||
public static final String CHANNEL_SWIPE_EVENT_LEFT = "LEFT";
|
||||
public static final String CHANNEL_SWIPE_EVENT_RIGHT = "RIGHT";
|
||||
|
||||
// List of light panel channels
|
||||
public static final String CHANNEL_PANEL_COLOR = "color";
|
||||
|
||||
@@ -12,8 +12,6 @@
|
||||
*/
|
||||
package org.openhab.binding.nanoleaf.internal;
|
||||
|
||||
import static org.openhab.binding.nanoleaf.internal.NanoleafBindingConstants.*;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
@@ -21,7 +19,6 @@ import java.util.stream.Stream;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.eclipse.jetty.client.HttpClient;
|
||||
import org.openhab.binding.nanoleaf.internal.handler.NanoleafControllerHandler;
|
||||
import org.openhab.binding.nanoleaf.internal.handler.NanoleafPanelHandler;
|
||||
import org.openhab.core.io.net.http.HttpClientFactory;
|
||||
@@ -48,35 +45,35 @@ import org.slf4j.LoggerFactory;
|
||||
@Component(configurationPid = "binding.nanoleaf", service = ThingHandlerFactory.class)
|
||||
public class NanoleafHandlerFactory extends BaseThingHandlerFactory {
|
||||
|
||||
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Collections
|
||||
.unmodifiableSet(Stream.of(THING_TYPE_LIGHT_PANEL, THING_TYPE_CONTROLLER).collect(Collectors.toSet()));
|
||||
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Collections.unmodifiableSet(
|
||||
Stream.of(NanoleafBindingConstants.THING_TYPE_LIGHT_PANEL, NanoleafBindingConstants.THING_TYPE_CONTROLLER)
|
||||
.collect(Collectors.toSet()));
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(NanoleafHandlerFactory.class);
|
||||
private final HttpClient httpClient;
|
||||
private final HttpClientFactory httpClientFactory;
|
||||
|
||||
@Activate
|
||||
public NanoleafHandlerFactory(@Reference final HttpClientFactory httpClientFactory) {
|
||||
this.httpClient = httpClientFactory.getCommonHttpClient();
|
||||
public NanoleafHandlerFactory(@Reference HttpClientFactory httpClientFactory) {
|
||||
this.httpClientFactory = httpClientFactory;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
|
||||
return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @Nullable ThingHandler createHandler(Thing thing) {
|
||||
@Nullable
|
||||
protected ThingHandler createHandler(Thing thing) {
|
||||
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
|
||||
|
||||
if (THING_TYPE_CONTROLLER.equals(thingTypeUID)) {
|
||||
NanoleafControllerHandler handler = new NanoleafControllerHandler((Bridge) thing, httpClient);
|
||||
if (NanoleafBindingConstants.THING_TYPE_CONTROLLER.equals(thingTypeUID)) {
|
||||
NanoleafControllerHandler handler = new NanoleafControllerHandler((Bridge) thing, this.httpClientFactory);
|
||||
logger.debug("Nanoleaf controller handler created.");
|
||||
return handler;
|
||||
} else if (THING_TYPE_LIGHT_PANEL.equals(thingTypeUID)) {
|
||||
NanoleafPanelHandler handler = new NanoleafPanelHandler(thing, httpClient);
|
||||
} else if (NanoleafBindingConstants.THING_TYPE_LIGHT_PANEL.equals(thingTypeUID)) {
|
||||
NanoleafPanelHandler handler = new NanoleafPanelHandler(thing, this.httpClientFactory);
|
||||
logger.debug("Nanoleaf panel handler created.");
|
||||
return handler;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,8 @@
|
||||
*/
|
||||
package org.openhab.binding.nanoleaf.internal;
|
||||
|
||||
import static org.openhab.binding.nanoleaf.internal.NanoleafBindingConstants.*;
|
||||
import static org.openhab.binding.nanoleaf.internal.NanoleafBindingConstants.API_ADD_USER;
|
||||
import static org.openhab.binding.nanoleaf.internal.NanoleafBindingConstants.API_V1_BASE_URL;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
@@ -45,20 +46,17 @@ import org.slf4j.LoggerFactory;
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class OpenAPIUtils {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(OpenAPIUtils.class);
|
||||
|
||||
// Regular expression for firmware version
|
||||
private static final Pattern FIRMWARE_VERSION_PATTERN = Pattern.compile("(\\d+)\\.(\\d+)\\.(\\d+)");
|
||||
private static final Pattern FIRMWARE_VERSION_PATTERN_BETA = Pattern.compile("(\\d+)\\.(\\d+)\\.(\\d+)-(\\d+)");
|
||||
private static final long CONNECT_TIMEOUT = 10L;
|
||||
|
||||
public static Request requestBuilder(HttpClient httpClient, NanoleafControllerConfig controllerConfig,
|
||||
String apiOperation, HttpMethod method) throws NanoleafException {
|
||||
URI requestURI = getUri(controllerConfig, apiOperation, null);
|
||||
LOGGER.trace("RequestBuilder: Sending Request {}:{} {} ", requestURI.getHost(), requestURI.getPort(),
|
||||
requestURI.getPath());
|
||||
|
||||
return httpClient.newRequest(requestURI).method(method).timeout(10, TimeUnit.SECONDS);
|
||||
LOGGER.trace("RequestBuilder: Sending Request {}:{} {} \n op: {} method: {}", new Object[] {
|
||||
requestURI.getHost(), requestURI.getPort(), requestURI.getPath(), apiOperation, method.toString() });
|
||||
return httpClient.newRequest(requestURI).method(method).timeout(CONNECT_TIMEOUT, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
public static URI getUri(NanoleafControllerConfig controllerConfig, String apiOperation, @Nullable String query)
|
||||
@@ -73,35 +71,33 @@ public class OpenAPIUtils {
|
||||
path = String.format("%s%s", API_V1_BASE_URL, apiOperation);
|
||||
} else {
|
||||
String authToken = controllerConfig.authToken;
|
||||
if (authToken != null) {
|
||||
path = String.format("%s/%s%s", API_V1_BASE_URL, authToken, apiOperation);
|
||||
} else {
|
||||
if (authToken == null) {
|
||||
throw new NanoleafUnauthorizedException("No authentication token found in configuration");
|
||||
}
|
||||
|
||||
path = String.format("%s/%s%s", API_V1_BASE_URL, authToken, apiOperation);
|
||||
}
|
||||
URI requestURI;
|
||||
|
||||
try {
|
||||
requestURI = new URI(HttpScheme.HTTP.asString(), null, address, port, path, query, null);
|
||||
} catch (URISyntaxException use) {
|
||||
URI requestURI = new URI(HttpScheme.HTTP.asString(), (String) null, address, port, path, query,
|
||||
(String) null);
|
||||
return requestURI;
|
||||
} catch (URISyntaxException var8) {
|
||||
LOGGER.warn("URI could not be parsed with path {}", path);
|
||||
throw new NanoleafException("Wrong URI format for API request");
|
||||
}
|
||||
return requestURI;
|
||||
}
|
||||
|
||||
public static ContentResponse sendOpenAPIRequest(Request request) throws NanoleafException {
|
||||
try {
|
||||
traceSendRequest(request);
|
||||
ContentResponse openAPIResponse;
|
||||
openAPIResponse = request.send();
|
||||
ContentResponse openAPIResponse = request.send();
|
||||
if (LOGGER.isTraceEnabled()) {
|
||||
LOGGER.trace("API response from Nanoleaf controller: {}", openAPIResponse.getContentAsString());
|
||||
}
|
||||
LOGGER.debug("API response code: {}", openAPIResponse.getStatus());
|
||||
int responseStatus = openAPIResponse.getStatus();
|
||||
if (responseStatus == HttpStatus.OK_200 || responseStatus == HttpStatus.NO_CONTENT_204) {
|
||||
return openAPIResponse;
|
||||
} else {
|
||||
if (responseStatus != HttpStatus.OK_200 && responseStatus != HttpStatus.NO_CONTENT_204) {
|
||||
if (openAPIResponse.getStatus() == HttpStatus.UNAUTHORIZED_401) {
|
||||
throw new NanoleafUnauthorizedException("OpenAPI request unauthorized");
|
||||
} else if (openAPIResponse.getStatus() == HttpStatus.NOT_FOUND_404) {
|
||||
@@ -114,60 +110,67 @@ public class OpenAPIUtils {
|
||||
throw new NanoleafException(String.format("OpenAPI request failed. HTTP response code %s",
|
||||
openAPIResponse.getStatus()));
|
||||
}
|
||||
} else {
|
||||
return openAPIResponse;
|
||||
}
|
||||
} catch (ExecutionException | TimeoutException clientException) {
|
||||
if (clientException.getCause() instanceof HttpResponseException
|
||||
&& ((HttpResponseException) clientException.getCause()).getResponse()
|
||||
.getStatus() == HttpStatus.UNAUTHORIZED_401) {
|
||||
} catch (ExecutionException ee) {
|
||||
Throwable cause = ee.getCause();
|
||||
if (cause != null && cause instanceof HttpResponseException
|
||||
&& ((HttpResponseException) cause).getResponse().getStatus() == HttpStatus.UNAUTHORIZED_401) {
|
||||
LOGGER.warn("OpenAPI request unauthorized. Invalid authorization token.");
|
||||
throw new NanoleafUnauthorizedException("Invalid authorization token");
|
||||
} else {
|
||||
throw new NanoleafException("Failed to send OpenAPI request (final)", ee);
|
||||
}
|
||||
throw new NanoleafException("Failed to send OpenAPI request", clientException);
|
||||
} catch (InterruptedException interruptedException) {
|
||||
throw new NanoleafInterruptedException("OpenAPI request has been interrupted", interruptedException);
|
||||
} catch (TimeoutException te) {
|
||||
LOGGER.warn("OpenAPI request failed with timeout", te);
|
||||
throw new NanoleafException("Failed to send OpenAPI request: Timeout", te);
|
||||
} catch (InterruptedException ie) {
|
||||
throw new NanoleafInterruptedException("OpenAPI request has been interrupted", ie);
|
||||
}
|
||||
}
|
||||
|
||||
private static void traceSendRequest(Request request) {
|
||||
if (!LOGGER.isTraceEnabled()) {
|
||||
return;
|
||||
}
|
||||
LOGGER.trace("Sending Request {} {}", request.getURI(),
|
||||
request.getQuery() == null ? "no query parameters" : request.getQuery());
|
||||
LOGGER.trace("Request method:{} uri:{} params{}\n", request.getMethod(), request.getURI(), request.getParams());
|
||||
if (request.getContent() != null) {
|
||||
Iterator<ByteBuffer> iter = request.getContent().iterator();
|
||||
if (iter != null) {
|
||||
if (LOGGER.isTraceEnabled()) {
|
||||
LOGGER.trace("Sending Request {} {}", request.getURI(),
|
||||
request.getQuery() == null ? "no query parameters" : request.getQuery());
|
||||
LOGGER.trace("Request method:{} uri:{} params{}\n", request.getMethod(), request.getURI(),
|
||||
request.getParams());
|
||||
if (request.getContent() != null) {
|
||||
Iterator<ByteBuffer> iter = request.getContent().iterator();
|
||||
while (iter.hasNext()) {
|
||||
@Nullable
|
||||
ByteBuffer buffer = iter.next();
|
||||
LOGGER.trace("Content {}", StandardCharsets.UTF_8.decode(buffer).toString());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean checkRequiredFirmware(@Nullable String modelId, @Nullable String currentFirmwareVersion) {
|
||||
if (modelId == null || currentFirmwareVersion == null) {
|
||||
if (modelId != null && currentFirmwareVersion != null) {
|
||||
int[] currentVer = getFirmwareVersionNumbers(currentFirmwareVersion);
|
||||
int[] requiredVer = getFirmwareVersionNumbers("NL22".equals(modelId) ? "1.5.0" : "1.1.0");
|
||||
|
||||
for (int i = 0; i < currentVer.length; ++i) {
|
||||
if (currentVer[i] != requiredVer[i]) {
|
||||
if (currentVer[i] > requiredVer[i]) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
int[] currentVer = getFirmwareVersionNumbers(currentFirmwareVersion);
|
||||
|
||||
int[] requiredVer = getFirmwareVersionNumbers(
|
||||
MODEL_ID_LIGHTPANELS.equals(modelId) ? API_MIN_FW_VER_LIGHTPANELS : API_MIN_FW_VER_CANVAS);
|
||||
|
||||
for (int i = 0; i < currentVer.length; i++) {
|
||||
if (currentVer[i] != requiredVer[i]) {
|
||||
return currentVer[i] > requiredVer[i];
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public static int[] getFirmwareVersionNumbers(String firmwareVersion) throws IllegalArgumentException {
|
||||
LOGGER.debug("firmwareVersion: {}", firmwareVersion);
|
||||
Matcher m = FIRMWARE_VERSION_PATTERN.matcher(firmwareVersion);
|
||||
|
||||
if (m.matches()) {
|
||||
return new int[] { Integer.parseInt(m.group(1)), Integer.parseInt(m.group(2)),
|
||||
Integer.parseInt(m.group(3)) };
|
||||
|
||||
@@ -59,6 +59,10 @@ public class NanoleafCommandExtension extends AbstractConsoleCommandExtension {
|
||||
ThingHandler handler = thing.getHandler();
|
||||
if (handler instanceof NanoleafControllerHandler) {
|
||||
NanoleafControllerHandler nanoleafControllerHandler = (NanoleafControllerHandler) handler;
|
||||
if (!handler.getThing().isEnabled()) {
|
||||
console.println(
|
||||
"The following Nanoleaf is NOT enabled as a Thing. Enable it first to view its layout.");
|
||||
}
|
||||
String layout = nanoleafControllerHandler.getLayout();
|
||||
console.println("Layout of Nanoleaf controller '" + thing.getUID().getAsString()
|
||||
+ "' with label '" + thing.getLabel() + "':" + System.lineSeparator());
|
||||
|
||||
@@ -15,7 +15,6 @@ package org.openhab.binding.nanoleaf.internal.commanddescription;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNull;
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.nanoleaf.internal.NanoleafBindingConstants;
|
||||
@@ -49,7 +48,11 @@ public class NanoleafCommandDescriptionProvider extends BaseDynamicCommandDescri
|
||||
@Override
|
||||
public void setThingHandler(ThingHandler handler) {
|
||||
this.bridgeHandler = (NanoleafControllerHandler) handler;
|
||||
bridgeHandler.registerControllerListener(this);
|
||||
NanoleafControllerHandler localHandler = this.bridgeHandler;
|
||||
if (localHandler != null) {
|
||||
localHandler.registerControllerListener(this);
|
||||
}
|
||||
|
||||
effectChannelUID = new ChannelUID(handler.getThing().getUID(), NanoleafBindingConstants.CHANNEL_EFFECT);
|
||||
}
|
||||
|
||||
@@ -60,18 +63,19 @@ public class NanoleafCommandDescriptionProvider extends BaseDynamicCommandDescri
|
||||
|
||||
@Override
|
||||
public void deactivate() {
|
||||
if (bridgeHandler != null) {
|
||||
bridgeHandler.unregisterControllerListener(this);
|
||||
NanoleafControllerHandler localHandler = this.bridgeHandler;
|
||||
if (localHandler != null) {
|
||||
localHandler.unregisterControllerListener(this);
|
||||
}
|
||||
super.deactivate();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onControllerInfoFetched(@NonNull ThingUID bridge, @NonNull ControllerInfo controllerInfo) {
|
||||
List<@NonNull String> effects = controllerInfo.getEffects().getEffectsList();
|
||||
public void onControllerInfoFetched(ThingUID bridge, ControllerInfo controllerInfo) {
|
||||
List<String> effects = controllerInfo.getEffects().getEffectsList();
|
||||
ChannelUID uid = effectChannelUID;
|
||||
if (effects != null && uid != null && uid.getThingUID().equals(bridge)) {
|
||||
List<@NonNull CommandOption> commandOptions = effects.stream() //
|
||||
List<CommandOption> commandOptions = effects.stream() //
|
||||
.map(effect -> new CommandOption(effect, effect)) //
|
||||
.collect(Collectors.toList());
|
||||
setCommandOptions(uid, commandOptions);
|
||||
|
||||
@@ -33,6 +33,7 @@ import org.openhab.core.config.discovery.AbstractDiscoveryService;
|
||||
import org.openhab.core.config.discovery.DiscoveryResult;
|
||||
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
|
||||
import org.openhab.core.thing.ThingUID;
|
||||
import org.openhab.core.thing.binding.BridgeHandler;
|
||||
import org.openhab.core.thing.binding.ThingHandler;
|
||||
import org.openhab.core.thing.binding.ThingHandlerService;
|
||||
import org.slf4j.Logger;
|
||||
@@ -64,8 +65,10 @@ public class NanoleafPanelsDiscoveryService extends AbstractDiscoveryService
|
||||
|
||||
@Override
|
||||
public void deactivate() {
|
||||
if (bridgeHandler != null) {
|
||||
bridgeHandler.unregisterControllerListener(this);
|
||||
NanoleafControllerHandler localBridgeHandler = bridgeHandler;
|
||||
if (localBridgeHandler != null) {
|
||||
Boolean result = localBridgeHandler.unregisterControllerListener(this);
|
||||
logger.debug("unregistration of controller was {}", result ? "successful" : "unsuccessful");
|
||||
}
|
||||
super.deactivate();
|
||||
}
|
||||
@@ -89,13 +92,16 @@ public class NanoleafPanelsDiscoveryService extends AbstractDiscoveryService
|
||||
|
||||
private void createResultsFromControllerInfo() {
|
||||
ThingUID bridgeUID;
|
||||
if (bridgeHandler != null) {
|
||||
bridgeUID = bridgeHandler.getThing().getUID();
|
||||
BridgeHandler localBridgeHandler = bridgeHandler;
|
||||
if (localBridgeHandler != null) {
|
||||
bridgeUID = localBridgeHandler.getThing().getUID();
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
if (controllerInfo != null) {
|
||||
final PanelLayout panelLayout = controllerInfo.getPanelLayout();
|
||||
|
||||
ControllerInfo localControllerInfo = controllerInfo;
|
||||
if (localControllerInfo != null) {
|
||||
final PanelLayout panelLayout = localControllerInfo.getPanelLayout();
|
||||
@Nullable
|
||||
Layout layout = panelLayout.getLayout();
|
||||
|
||||
@@ -133,7 +139,9 @@ public class NanoleafPanelsDiscoveryService extends AbstractDiscoveryService
|
||||
@Override
|
||||
public void setThingHandler(ThingHandler handler) {
|
||||
this.bridgeHandler = (NanoleafControllerHandler) handler;
|
||||
this.bridgeHandler.registerControllerListener(this);
|
||||
NanoleafControllerHandler localBridgeHandler = (NanoleafControllerHandler) handler;
|
||||
|
||||
localBridgeHandler.registerControllerListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -15,7 +15,6 @@ package org.openhab.binding.nanoleaf.internal.handler;
|
||||
import static org.openhab.binding.nanoleaf.internal.NanoleafBindingConstants.*;
|
||||
|
||||
import java.net.URI;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
@@ -33,11 +32,10 @@ import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.eclipse.jetty.client.HttpClient;
|
||||
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.api.Result;
|
||||
import org.eclipse.jetty.client.util.StringContentProvider;
|
||||
import org.eclipse.jetty.http.HttpMethod;
|
||||
import org.eclipse.jetty.http.HttpStatus;
|
||||
import org.openhab.binding.nanoleaf.internal.NanoleafBindingConstants;
|
||||
import org.openhab.binding.nanoleaf.internal.NanoleafControllerListener;
|
||||
import org.openhab.binding.nanoleaf.internal.NanoleafException;
|
||||
import org.openhab.binding.nanoleaf.internal.NanoleafUnauthorizedException;
|
||||
@@ -55,11 +53,13 @@ import org.openhab.binding.nanoleaf.internal.model.Hue;
|
||||
import org.openhab.binding.nanoleaf.internal.model.IntegerState;
|
||||
import org.openhab.binding.nanoleaf.internal.model.Layout;
|
||||
import org.openhab.binding.nanoleaf.internal.model.On;
|
||||
import org.openhab.binding.nanoleaf.internal.model.PanelLayout;
|
||||
import org.openhab.binding.nanoleaf.internal.model.Rhythm;
|
||||
import org.openhab.binding.nanoleaf.internal.model.Sat;
|
||||
import org.openhab.binding.nanoleaf.internal.model.State;
|
||||
import org.openhab.binding.nanoleaf.internal.model.TouchEvents;
|
||||
import org.openhab.core.config.core.Configuration;
|
||||
import org.openhab.core.io.net.http.HttpClientFactory;
|
||||
import org.openhab.core.library.types.DecimalType;
|
||||
import org.openhab.core.library.types.HSBType;
|
||||
import org.openhab.core.library.types.IncreaseDecreaseType;
|
||||
@@ -94,20 +94,21 @@ public class NanoleafControllerHandler extends BaseBridgeHandler {
|
||||
|
||||
// Pairing interval in seconds
|
||||
private static final int PAIRING_INTERVAL = 10;
|
||||
private static final int CONNECT_TIMEOUT = 10;
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(NanoleafControllerHandler.class);
|
||||
private HttpClientFactory httpClientFactory;
|
||||
private HttpClient httpClient;
|
||||
private List<NanoleafControllerListener> controllerListeners = new CopyOnWriteArrayList<>();
|
||||
|
||||
// Pairing, update and panel discovery jobs and touch event job
|
||||
private @Nullable HttpClient httpClientSSETouchEvent;
|
||||
private @Nullable Request sseTouchjobRequest;
|
||||
private List<NanoleafControllerListener> controllerListeners = new CopyOnWriteArrayList<NanoleafControllerListener>();
|
||||
|
||||
private @NonNullByDefault({}) ScheduledFuture<?> pairingJob;
|
||||
private @NonNullByDefault({}) ScheduledFuture<?> updateJob;
|
||||
private @NonNullByDefault({}) ScheduledFuture<?> touchJob;
|
||||
|
||||
// JSON parser for API responses
|
||||
private final Gson gson = new Gson();
|
||||
|
||||
// Controller configuration settings and channel values
|
||||
private @Nullable String address;
|
||||
private int port;
|
||||
private int refreshIntervall;
|
||||
@@ -115,12 +116,34 @@ public class NanoleafControllerHandler extends BaseBridgeHandler {
|
||||
private @Nullable String deviceType;
|
||||
private @NonNullByDefault({}) ControllerInfo controllerInfo;
|
||||
|
||||
public NanoleafControllerHandler(Bridge bridge, HttpClient httpClient) {
|
||||
private boolean touchJobRunning = false;
|
||||
|
||||
public NanoleafControllerHandler(Bridge bridge, HttpClientFactory httpClientFactory) {
|
||||
super(bridge);
|
||||
this.httpClient = httpClient;
|
||||
this.httpClientFactory = httpClientFactory;
|
||||
this.httpClient = httpClientFactory.getCommonHttpClient();
|
||||
}
|
||||
|
||||
private void initializeTouchHttpClient() {
|
||||
String httpClientName = thing.getUID().getId();
|
||||
|
||||
try {
|
||||
httpClientSSETouchEvent = httpClientFactory.createHttpClient(httpClientName);
|
||||
final HttpClient localHttpClientSSETouchEvent = this.httpClientSSETouchEvent;
|
||||
if (localHttpClientSSETouchEvent != null) {
|
||||
localHttpClientSSETouchEvent.setConnectTimeout(CONNECT_TIMEOUT * 1000L);
|
||||
localHttpClientSSETouchEvent.start();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error(
|
||||
"Long running HttpClient for Nanoleaf controller handler {} cannot be started. Creating Handler failed.",
|
||||
httpClientName);
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
|
||||
}
|
||||
|
||||
logger.debug("Using long SSE httpClient={} for {}}", httpClientSSETouchEvent, httpClientName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
logger.debug("Initializing the controller (bridge)");
|
||||
updateStatus(ThingStatus.UNKNOWN);
|
||||
@@ -128,42 +151,45 @@ public class NanoleafControllerHandler extends BaseBridgeHandler {
|
||||
setAddress(config.address);
|
||||
setPort(config.port);
|
||||
setRefreshIntervall(config.refreshInterval);
|
||||
setAuthToken(config.authToken);
|
||||
|
||||
String authToken = (config.authToken != null) ? config.authToken : "";
|
||||
setAuthToken(authToken);
|
||||
Map<String, String> properties = getThing().getProperties();
|
||||
String propertyModelId = properties.get(Thing.PROPERTY_MODEL_ID);
|
||||
if (hasTouchSupport(propertyModelId)) {
|
||||
config.deviceType = DEVICE_TYPE_TOUCHSUPPORT;
|
||||
initializeTouchHttpClient();
|
||||
} else {
|
||||
config.deviceType = DEVICE_TYPE_LIGHTPANELS;
|
||||
}
|
||||
setDeviceType(config.deviceType);
|
||||
|
||||
setDeviceType(config.deviceType);
|
||||
String propertyFirmwareVersion = properties.get(Thing.PROPERTY_FIRMWARE_VERSION);
|
||||
|
||||
try {
|
||||
if (config.address.isEmpty() || String.valueOf(config.port).isEmpty()) {
|
||||
if (!config.address.isEmpty() && !String.valueOf(config.port).isEmpty()) {
|
||||
if (propertyFirmwareVersion != null && !propertyFirmwareVersion.isEmpty() && !OpenAPIUtils
|
||||
.checkRequiredFirmware(properties.get(Thing.PROPERTY_MODEL_ID), propertyFirmwareVersion)) {
|
||||
logger.warn("Nanoleaf controller firmware is too old: {}. Must be equal or higher than {}",
|
||||
propertyFirmwareVersion, API_MIN_FW_VER_LIGHTPANELS);
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
|
||||
"@text/error.nanoleaf.controller.incompatibleFirmware");
|
||||
stopAllJobs();
|
||||
} else if (authToken != null && !authToken.isEmpty()) {
|
||||
stopPairingJob();
|
||||
startUpdateJob();
|
||||
startTouchJob();
|
||||
} else {
|
||||
logger.debug("No token found. Start pairing background job");
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING,
|
||||
"@text/error.nanoleaf.controller.noToken");
|
||||
startPairingJob();
|
||||
stopUpdateJob();
|
||||
}
|
||||
} else {
|
||||
logger.warn("No IP address and port configured for the Nanoleaf controller");
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING,
|
||||
"@text/error.nanoleaf.controller.noIp");
|
||||
stopAllJobs();
|
||||
} else if (propertyFirmwareVersion != null && !propertyFirmwareVersion.isEmpty() && !OpenAPIUtils
|
||||
.checkRequiredFirmware(properties.get(Thing.PROPERTY_MODEL_ID), propertyFirmwareVersion)) {
|
||||
logger.warn("Nanoleaf controller firmware is too old: {}. Must be equal or higher than {}",
|
||||
propertyFirmwareVersion, API_MIN_FW_VER_LIGHTPANELS);
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
|
||||
"@text/error.nanoleaf.controller.incompatibleFirmware");
|
||||
stopAllJobs();
|
||||
} else if (config.authToken == null || config.authToken.isEmpty()) {
|
||||
logger.debug("No token found. Start pairing background job");
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING,
|
||||
"@text/error.nanoleaf.controller.noToken");
|
||||
startPairingJob();
|
||||
stopUpdateJob();
|
||||
} else {
|
||||
stopPairingJob();
|
||||
startUpdateJob();
|
||||
startTouchJob();
|
||||
}
|
||||
} catch (IllegalArgumentException iae) {
|
||||
logger.warn("Nanoleaf controller firmware version not in format x.y.z: {}",
|
||||
@@ -173,55 +199,52 @@ public class NanoleafControllerHandler extends BaseBridgeHandler {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleCommand(ChannelUID channelUID, Command command) {
|
||||
logger.debug("Received command {} for channel {}", command, channelUID);
|
||||
if (!ThingStatus.ONLINE.equals(getThing().getStatusInfo().getStatus())) {
|
||||
logger.debug("Cannot handle command. Bridge is not online.");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (command instanceof RefreshType) {
|
||||
updateFromControllerInfo();
|
||||
} else {
|
||||
switch (channelUID.getId()) {
|
||||
case CHANNEL_COLOR:
|
||||
case CHANNEL_COLOR_TEMPERATURE:
|
||||
case CHANNEL_COLOR_TEMPERATURE_ABS:
|
||||
sendStateCommand(channelUID.getId(), command);
|
||||
break;
|
||||
case CHANNEL_EFFECT:
|
||||
sendEffectCommand(command);
|
||||
break;
|
||||
case CHANNEL_RHYTHM_MODE:
|
||||
sendRhythmCommand(command);
|
||||
break;
|
||||
default:
|
||||
logger.warn("Channel with id {} not handled", channelUID.getId());
|
||||
break;
|
||||
} else {
|
||||
try {
|
||||
if (command instanceof RefreshType) {
|
||||
updateFromControllerInfo();
|
||||
} else {
|
||||
switch (channelUID.getId()) {
|
||||
case CHANNEL_COLOR:
|
||||
case CHANNEL_COLOR_TEMPERATURE:
|
||||
case CHANNEL_COLOR_TEMPERATURE_ABS:
|
||||
sendStateCommand(channelUID.getId(), command);
|
||||
break;
|
||||
case CHANNEL_EFFECT:
|
||||
sendEffectCommand(command);
|
||||
break;
|
||||
case CHANNEL_RHYTHM_MODE:
|
||||
sendRhythmCommand(command);
|
||||
break;
|
||||
default:
|
||||
logger.warn("Channel with id {} not handled", channelUID.getId());
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (NanoleafUnauthorizedException nue) {
|
||||
logger.debug("Authorization for command {} to channelUID {} failed: {}", command, channelUID,
|
||||
nue.getMessage());
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
|
||||
"@text/error.nanoleaf.controller.invalidToken");
|
||||
} catch (NanoleafException ne) {
|
||||
logger.debug("Handling command {} to channelUID {} failed: {}", command, channelUID, ne.getMessage());
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
|
||||
"@text/error.nanoleaf.controller.communication");
|
||||
}
|
||||
} catch (NanoleafUnauthorizedException nae) {
|
||||
logger.warn("Authorization for command {} to channelUID {} failed: {}", command, channelUID,
|
||||
nae.getMessage());
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
|
||||
"@text/error.nanoleaf.controller.invalidToken");
|
||||
} catch (NanoleafException ne) {
|
||||
logger.warn("Handling command {} to channelUID {} failed: {}", command, channelUID, ne.getMessage());
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
|
||||
"@text/error.nanoleaf.controller.communication");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleRemoval() {
|
||||
scheduler.execute(() -> {
|
||||
// delete token for openHAB
|
||||
ContentResponse deleteTokenResponse;
|
||||
try {
|
||||
Request deleteTokenRequest = OpenAPIUtils.requestBuilder(httpClient, getControllerConfig(),
|
||||
API_DELETE_USER, HttpMethod.DELETE);
|
||||
deleteTokenResponse = OpenAPIUtils.sendOpenAPIRequest(deleteTokenRequest);
|
||||
ContentResponse deleteTokenResponse = OpenAPIUtils.sendOpenAPIRequest(deleteTokenRequest);
|
||||
if (deleteTokenResponse.getStatus() != HttpStatus.NO_CONTENT_204) {
|
||||
logger.warn("Failed to delete token for openHAB. Response code is {}",
|
||||
deleteTokenResponse.getStatus());
|
||||
@@ -272,32 +295,38 @@ public class NanoleafControllerHandler extends BaseBridgeHandler {
|
||||
}
|
||||
|
||||
public String getLayout() {
|
||||
Layout layout = controllerInfo.getPanelLayout().getLayout();
|
||||
String layoutView = (layout != null) ? layout.getLayoutView() : "";
|
||||
String layoutView = "";
|
||||
if (controllerInfo != null) {
|
||||
PanelLayout panelLayout = controllerInfo.getPanelLayout();
|
||||
Layout layout = panelLayout.getLayout();
|
||||
layoutView = layout != null ? layout.getLayoutView() : "";
|
||||
}
|
||||
|
||||
return layoutView;
|
||||
}
|
||||
|
||||
public synchronized void startPairingJob() {
|
||||
if (pairingJob == null || pairingJob.isCancelled()) {
|
||||
logger.debug("Start pairing job, interval={} sec", PAIRING_INTERVAL);
|
||||
pairingJob = scheduler.scheduleWithFixedDelay(this::runPairing, 0, PAIRING_INTERVAL, TimeUnit.SECONDS);
|
||||
pairingJob = scheduler.scheduleWithFixedDelay(this::runPairing, 0L, PAIRING_INTERVAL, TimeUnit.SECONDS);
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized void stopPairingJob() {
|
||||
logger.debug("Stop pairing job {}", pairingJob != null ? pairingJob.isCancelled() : "pairing job = null");
|
||||
if (pairingJob != null && !pairingJob.isCancelled()) {
|
||||
logger.debug("Stop pairing job");
|
||||
pairingJob.cancel(true);
|
||||
this.pairingJob = null;
|
||||
pairingJob = null;
|
||||
logger.debug("Stopped pairing job");
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized void startUpdateJob() {
|
||||
String localAuthToken = getAuthToken();
|
||||
final String localAuthToken = getAuthToken();
|
||||
if (localAuthToken != null && !localAuthToken.isEmpty()) {
|
||||
if (updateJob == null || updateJob.isCancelled()) {
|
||||
logger.debug("Start controller status job, repeat every {} sec", getRefreshInterval());
|
||||
updateJob = scheduler.scheduleWithFixedDelay(this::runUpdate, 0, getRefreshInterval(),
|
||||
updateJob = scheduler.scheduleWithFixedDelay(this::runUpdate, 0L, getRefreshInterval(),
|
||||
TimeUnit.SECONDS);
|
||||
}
|
||||
} else {
|
||||
@@ -307,126 +336,146 @@ public class NanoleafControllerHandler extends BaseBridgeHandler {
|
||||
}
|
||||
|
||||
private synchronized void stopUpdateJob() {
|
||||
logger.debug("Stop update job {}", updateJob != null ? updateJob.isCancelled() : "update job = null");
|
||||
if (updateJob != null && !updateJob.isCancelled()) {
|
||||
logger.debug("Stop status job");
|
||||
updateJob.cancel(true);
|
||||
this.updateJob = null;
|
||||
updateJob = null;
|
||||
logger.debug("Stopped status job");
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized void startTouchJob() {
|
||||
NanoleafControllerConfig config = getConfigAs(NanoleafControllerConfig.class);
|
||||
if (!config.deviceType.equals(DEVICE_TYPE_TOUCHSUPPORT)) {
|
||||
logger.debug("NOT starting TouchJob for Panel {} because it has wrong device type '{}' vs required '{}'",
|
||||
logger.debug(
|
||||
"NOT starting TouchJob for Controller {} because it has wrong device type '{}' vs required '{}'",
|
||||
this.getThing().getUID(), config.deviceType, DEVICE_TYPE_TOUCHSUPPORT);
|
||||
return;
|
||||
} else {
|
||||
logger.debug("Starting TouchJob for Panel {}", this.getThing().getUID());
|
||||
}
|
||||
|
||||
String localAuthToken = getAuthToken();
|
||||
if (localAuthToken != null && !localAuthToken.isEmpty()) {
|
||||
if (touchJob == null || touchJob.isCancelled()) {
|
||||
logger.debug("Starting Touchjob now");
|
||||
touchJob = scheduler.schedule(this::runTouchDetection, 0, TimeUnit.SECONDS);
|
||||
logger.debug("Starting TouchJob for Controller {}", getThing().getUID());
|
||||
final String localAuthToken = getAuthToken();
|
||||
if (localAuthToken != null && !localAuthToken.isEmpty()) {
|
||||
if (touchJob != null && !touchJob.isDone()) {
|
||||
logger.trace("tj: tj={} already running touchJobRunning = {} cancelled={} done={}", touchJob,
|
||||
touchJobRunning, touchJob == null ? null : touchJob.isCancelled(),
|
||||
touchJob == null ? null : touchJob.isDone());
|
||||
} else {
|
||||
logger.debug("tj: Starting NEW touch job : tj={} touchJobRunning={} cancelled={} done={}",
|
||||
touchJob, touchJobRunning, touchJob == null ? null : touchJob.isCancelled(),
|
||||
touchJob == null ? null : touchJob.isDone());
|
||||
touchJob = scheduler.scheduleWithFixedDelay(this::runTouchDetection, 0L, 1L, TimeUnit.SECONDS);
|
||||
}
|
||||
} else {
|
||||
logger.error("starting TouchJob for Controller {} failed - missing token", getThing().getUID());
|
||||
}
|
||||
} else {
|
||||
logger.error("starting TouchJob for Controller {} failed - missing token", this.getThing().getUID());
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized void stopTouchJob() {
|
||||
logger.debug("Stop touch job {}", touchJob != null ? touchJob.isCancelled() : "touchJob job = null");
|
||||
if (touchJob != null) {
|
||||
logger.trace("tj: touch job stopping for {} with client {}", thing.getUID(), httpClientSSETouchEvent);
|
||||
|
||||
final Request localSSERequest = sseTouchjobRequest;
|
||||
if (localSSERequest != null) {
|
||||
localSSERequest.abort(new NanoleafException("Touch detection stopped"));
|
||||
}
|
||||
if (!touchJob.isCancelled()) {
|
||||
touchJob.cancel(true);
|
||||
}
|
||||
|
||||
touchJob = null;
|
||||
touchJobRunning = false;
|
||||
logger.debug("tj: touch job stopped for {} with client {}", thing.getUID(), httpClientSSETouchEvent);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean hasTouchSupport(@Nullable String deviceType) {
|
||||
return (MODELS_WITH_TOUCHSUPPORT.contains(deviceType));
|
||||
}
|
||||
|
||||
private synchronized void stopTouchJob() {
|
||||
if (touchJob != null && !touchJob.isCancelled()) {
|
||||
logger.debug("Stop touch job");
|
||||
touchJob.cancel(true);
|
||||
this.touchJob = null;
|
||||
}
|
||||
return NanoleafBindingConstants.MODELS_WITH_TOUCHSUPPORT.contains(deviceType);
|
||||
}
|
||||
|
||||
private void runUpdate() {
|
||||
logger.debug("Run update job");
|
||||
|
||||
try {
|
||||
updateFromControllerInfo();
|
||||
startTouchJob(); // if device type has changed, start touch detection.
|
||||
startTouchJob();
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
} catch (NanoleafUnauthorizedException nae) {
|
||||
logger.warn("Status update unauthorized: {}", nae.getMessage());
|
||||
logger.debug("Status update unauthorized for controller {}: {}", getThing().getUID(), nae.getMessage());
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
|
||||
"@text/error.nanoleaf.controller.invalidToken");
|
||||
String localAuthToken = getAuthToken();
|
||||
final String localAuthToken = getAuthToken();
|
||||
if (localAuthToken == null || localAuthToken.isEmpty()) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING,
|
||||
"@text/error.nanoleaf.controller.noToken");
|
||||
}
|
||||
} catch (NanoleafException ne) {
|
||||
logger.warn("Status update failed: {}", ne.getMessage());
|
||||
logger.debug("Status update failed for controller {} : {}", getThing().getUID(), ne.getMessage());
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
|
||||
"@text/error.nanoleaf.controller.communication");
|
||||
} catch (RuntimeException e) {
|
||||
logger.warn("Update job failed", e);
|
||||
logger.debug("Update job failed", e);
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "@text/error.nanoleaf.controller.runtime");
|
||||
}
|
||||
}
|
||||
|
||||
private void runPairing() {
|
||||
logger.debug("Run pairing job");
|
||||
|
||||
try {
|
||||
String localAuthToken = getAuthToken();
|
||||
final String localAuthToken = getAuthToken();
|
||||
if (localAuthToken != null && !localAuthToken.isEmpty()) {
|
||||
if (pairingJob != null) {
|
||||
pairingJob.cancel(false);
|
||||
}
|
||||
|
||||
logger.debug("Authentication token found. Canceling pairing job");
|
||||
return;
|
||||
}
|
||||
|
||||
ContentResponse authTokenResponse = OpenAPIUtils
|
||||
.requestBuilder(httpClient, getControllerConfig(), API_ADD_USER, HttpMethod.POST)
|
||||
.timeout(20, TimeUnit.SECONDS).send();
|
||||
.timeout(20L, TimeUnit.SECONDS).send();
|
||||
String authTokenResponseString = (authTokenResponse != null) ? authTokenResponse.getContentAsString() : "";
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Auth token response: {}", authTokenResponse.getContentAsString());
|
||||
logger.trace("Auth token response: {}", authTokenResponseString);
|
||||
}
|
||||
|
||||
if (authTokenResponse.getStatus() != HttpStatus.OK_200) {
|
||||
logger.debug("Pairing pending for {}. Controller returns status code {}", this.getThing().getUID(),
|
||||
if (authTokenResponse != null && authTokenResponse.getStatus() != HttpStatus.OK_200) {
|
||||
logger.debug("Pairing pending for {}. Controller returns status code {}", getThing().getUID(),
|
||||
authTokenResponse.getStatus());
|
||||
} else {
|
||||
// get auth token from response
|
||||
AuthToken authTokenObject = gson.fromJson(authTokenResponse.getContentAsString(), AuthToken.class);
|
||||
localAuthToken = authTokenObject.getAuthToken();
|
||||
if (localAuthToken != null && !localAuthToken.isEmpty()) {
|
||||
logger.debug("Pairing succeeded.");
|
||||
|
||||
// Update and save the auth token in the thing configuration
|
||||
Configuration config = editConfiguration();
|
||||
config.put(NanoleafControllerConfig.AUTH_TOKEN, localAuthToken);
|
||||
updateConfiguration(config);
|
||||
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
// Update local field
|
||||
setAuthToken(localAuthToken);
|
||||
|
||||
stopPairingJob();
|
||||
startUpdateJob();
|
||||
startTouchJob();
|
||||
} else {
|
||||
logger.debug("No auth token found in response: {}", authTokenResponse.getContentAsString());
|
||||
AuthToken authTokenObject = gson.fromJson(authTokenResponseString, AuthToken.class);
|
||||
authTokenObject = (authTokenObject != null) ? authTokenObject : new AuthToken();
|
||||
if (authTokenObject.getAuthToken().isEmpty()) {
|
||||
logger.debug("No auth token found in response: {}", authTokenResponseString);
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
|
||||
"@text/error.nanoleaf.controller.pairingFailed");
|
||||
throw new NanoleafException(authTokenResponse.getContentAsString());
|
||||
throw new NanoleafException(authTokenResponseString);
|
||||
}
|
||||
|
||||
logger.debug("Pairing succeeded.");
|
||||
Configuration config = editConfiguration();
|
||||
|
||||
config.put(NanoleafControllerConfig.AUTH_TOKEN, authTokenObject.getAuthToken());
|
||||
updateConfiguration(config);
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
// Update local field
|
||||
setAuthToken(authTokenObject.getAuthToken());
|
||||
|
||||
stopPairingJob();
|
||||
startUpdateJob();
|
||||
startTouchJob();
|
||||
}
|
||||
} catch (JsonSyntaxException e) {
|
||||
logger.warn("Received invalid data", e);
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
|
||||
"@text/error.nanoleaf.controller.invalidData");
|
||||
} catch (NanoleafException e) {
|
||||
} catch (NanoleafException ne) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
|
||||
"@text/error.nanoleaf.controller.noTokenReceived");
|
||||
} catch (InterruptedException | ExecutionException | TimeoutException e) {
|
||||
} catch (ExecutionException | TimeoutException | InterruptedException e) {
|
||||
logger.debug("Cannot send authorization request to controller: ", e);
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
|
||||
"@text/error.nanoleaf.controller.authRequest");
|
||||
@@ -440,133 +489,159 @@ public class NanoleafControllerHandler extends BaseBridgeHandler {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This is based on the touch event detection described in https://forum.nanoleaf.me/docs/openapi#_842h3097vbgq
|
||||
*/
|
||||
private static boolean touchJobRunning = false;
|
||||
|
||||
private void runTouchDetection() {
|
||||
if (touchJobRunning) {
|
||||
logger.debug("touch job already running. quitting.");
|
||||
return;
|
||||
private synchronized void runTouchDetection() {
|
||||
final HttpClient localhttpSSEClientTouchEvent = httpClientSSETouchEvent;
|
||||
int eventHashcode = -1;
|
||||
if (localhttpSSEClientTouchEvent != null) {
|
||||
eventHashcode = localhttpSSEClientTouchEvent.hashCode();
|
||||
}
|
||||
try {
|
||||
touchJobRunning = true;
|
||||
URI eventUri = OpenAPIUtils.getUri(getControllerConfig(), API_EVENTS, "id=4");
|
||||
logger.debug("touch job registered on: {}", eventUri.toString());
|
||||
httpClient.newRequest(eventUri).send(new Response.Listener.Adapter() // request runs forever
|
||||
{
|
||||
@Override
|
||||
public void onContent(@Nullable Response response, @Nullable ByteBuffer content) {
|
||||
String s = StandardCharsets.UTF_8.decode(content).toString();
|
||||
logger.trace("content {}", s);
|
||||
if (touchJobRunning) {
|
||||
logger.trace("tj: touch job {} touch job already running. quitting. {} controller {} with {}\",\n",
|
||||
touchJob, eventHashcode, thing.getUID(), httpClientSSETouchEvent);
|
||||
} else {
|
||||
try {
|
||||
URI eventUri = OpenAPIUtils.getUri(getControllerConfig(), API_EVENTS, "id=4");
|
||||
logger.debug("tj: touch job request registering for {} with client {}", thing.getUID(),
|
||||
httpClientSSETouchEvent);
|
||||
touchJobRunning = true;
|
||||
if (localhttpSSEClientTouchEvent != null) {
|
||||
localhttpSSEClientTouchEvent.setIdleTimeout(CONNECT_TIMEOUT * 1000L);
|
||||
sseTouchjobRequest = localhttpSSEClientTouchEvent.newRequest(eventUri);
|
||||
final Request localSSETouchjobRequest = sseTouchjobRequest;
|
||||
int requestHashCode = -1;
|
||||
if (localSSETouchjobRequest != null) {
|
||||
requestHashCode = localSSETouchjobRequest.hashCode();
|
||||
|
||||
Scanner eventContent = new Scanner(s);
|
||||
while (eventContent.hasNextLine()) {
|
||||
String line = eventContent.nextLine().trim();
|
||||
// we don't expect anything than content id:4, so we do not check that but only care about the
|
||||
// data part
|
||||
if (line.startsWith("data:")) {
|
||||
String json = line.substring(5).trim(); // supposed to be JSON
|
||||
try {
|
||||
TouchEvents touchEvents = gson.fromJson(json, TouchEvents.class);
|
||||
handleTouchEvents(Objects.requireNonNull(touchEvents));
|
||||
} catch (JsonSyntaxException jse) {
|
||||
logger.error("couldn't parse touch event json {}", json);
|
||||
logger.debug("tj: triggering new touch job request {} for {} with client {}", requestHashCode,
|
||||
thing.getUID(), eventHashcode);
|
||||
localSSETouchjobRequest.onResponseContent((response, content) -> {
|
||||
String s = StandardCharsets.UTF_8.decode(content).toString();
|
||||
logger.debug("touch detected for controller {}", thing.getUID());
|
||||
logger.trace("content {}", s);
|
||||
Scanner eventContent = new Scanner(s);
|
||||
|
||||
while (eventContent.hasNextLine()) {
|
||||
String line = eventContent.nextLine().trim();
|
||||
if (line.startsWith("data:")) {
|
||||
String json = line.substring(5).trim();
|
||||
|
||||
try {
|
||||
TouchEvents touchEvents = gson.fromJson(json, TouchEvents.class);
|
||||
handleTouchEvents(Objects.requireNonNull(touchEvents));
|
||||
} catch (JsonSyntaxException e) {
|
||||
logger.error("Couldn't parse touch event json {}", json);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
eventContent.close();
|
||||
logger.debug("leaving touch onContent");
|
||||
}).onResponseSuccess((response) -> {
|
||||
logger.trace("tj: r={} touch event SUCCESS: {}", response.getRequest(), response);
|
||||
}).onResponseFailure((response, failure) -> {
|
||||
logger.trace("tj: r={} touch event FAILURE. Touchjob not running anymore for controller {}",
|
||||
response.getRequest(), thing.getUID());
|
||||
}).send((result) -> {
|
||||
logger.trace(
|
||||
"tj: r={} touch event COMPLETE. Touchjob not running anymore for controller {} failed: {} succeeded: {}",
|
||||
result.getRequest(), thing.getUID(), result.isFailed(), result.isSucceeded());
|
||||
touchJobRunning = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
logger.trace("tj: started touch job request for {} with {} at {}", thing.getUID(),
|
||||
httpClientSSETouchEvent, eventUri);
|
||||
} catch (NanoleafException | RuntimeException e) {
|
||||
logger.warn("tj: setting up TouchDetection failed for controller {} with {}\",\n", thing.getUID(),
|
||||
httpClientSSETouchEvent);
|
||||
logger.warn("tj: setting up TouchDetection failed with exception", e);
|
||||
} finally {
|
||||
logger.trace("tj: touch job {} started for new request {} controller {} with {}\",\n",
|
||||
touchJob.hashCode(), eventHashcode, thing.getUID(), httpClientSSETouchEvent);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private void handleTouchEvents(TouchEvents touchEvents) {
|
||||
touchEvents.getEvents().forEach((event) -> {
|
||||
logger.info("panel: {} gesture id: {}", event.getPanelId(), event.getGesture());
|
||||
// Swipes go to the controller, taps go to the individual panel
|
||||
if (event.getPanelId().equals(CONTROLLER_PANEL_ID)) {
|
||||
logger.debug("Triggering controller {} with gesture {}.", thing.getUID(), event.getGesture());
|
||||
updateControllerGesture(event.getGesture());
|
||||
} else {
|
||||
getThing().getThings().forEach((child) -> {
|
||||
NanoleafPanelHandler panelHandler = (NanoleafPanelHandler) child.getHandler();
|
||||
if (panelHandler != null) {
|
||||
logger.trace("Checking available panel -{}- versus event panel -{}-", panelHandler.getPanelID(),
|
||||
event.getPanelId());
|
||||
if (panelHandler.getPanelID().equals(event.getPanelId())) {
|
||||
logger.debug("Panel {} found. Triggering item with gesture {}.", panelHandler.getPanelID(),
|
||||
event.getGesture());
|
||||
panelHandler.updatePanelGesture(event.getGesture());
|
||||
}
|
||||
}
|
||||
eventContent.close();
|
||||
logger.debug("leaving touch onContent");
|
||||
super.onContent(response, content);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSuccess(@Nullable Response response) {
|
||||
logger.trace("touch event SUCCESS: {}", response);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(@Nullable Response response, @Nullable Throwable failure) {
|
||||
logger.trace("touch event FAILURE: {}", response);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onComplete(@Nullable Result result) {
|
||||
logger.trace("touch event COMPLETE: {}", result);
|
||||
}
|
||||
});
|
||||
} catch (RuntimeException | NanoleafException e) {
|
||||
logger.warn("setting up TouchDetection failed", e);
|
||||
} finally {
|
||||
touchJobRunning = false;
|
||||
}
|
||||
logger.debug("leaving run touch detection");
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Interate over all gathered touch events and apply them to the panel they belong to
|
||||
* Apply the swipe gesture to the controller
|
||||
*
|
||||
* @param touchEvents
|
||||
* @param gesture Only swipes are supported on the complete nanoleaf panels
|
||||
*/
|
||||
private void handleTouchEvents(TouchEvents touchEvents) {
|
||||
touchEvents.getEvents().forEach(event -> {
|
||||
logger.info("panel: {} gesture id: {}", event.getPanelId(), event.getGesture());
|
||||
|
||||
// Iterate over all child things = all panels of that controller
|
||||
this.getThing().getThings().forEach(child -> {
|
||||
NanoleafPanelHandler panelHandler = (NanoleafPanelHandler) child.getHandler();
|
||||
if (panelHandler != null) {
|
||||
logger.trace("Checking available panel -{}- versus event panel -{}-", panelHandler.getPanelID(),
|
||||
event.getPanelId());
|
||||
if (panelHandler.getPanelID().equals(event.getPanelId())) {
|
||||
logger.debug("Panel {} found. Triggering item with gesture {}.", panelHandler.getPanelID(),
|
||||
event.getGesture());
|
||||
panelHandler.updatePanelGesture(event.getGesture());
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
private void updateControllerGesture(int gesture) {
|
||||
switch (gesture) {
|
||||
case 2:
|
||||
triggerChannel(CHANNEL_SWIPE, CHANNEL_SWIPE_EVENT_UP);
|
||||
break;
|
||||
case 3:
|
||||
triggerChannel(CHANNEL_SWIPE, CHANNEL_SWIPE_EVENT_DOWN);
|
||||
break;
|
||||
case 4:
|
||||
triggerChannel(CHANNEL_SWIPE, CHANNEL_SWIPE_EVENT_LEFT);
|
||||
break;
|
||||
case 5:
|
||||
triggerChannel(CHANNEL_SWIPE, CHANNEL_SWIPE_EVENT_RIGHT);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void updateFromControllerInfo() throws NanoleafException {
|
||||
logger.debug("Update channels for controller {}", thing.getUID());
|
||||
this.controllerInfo = receiveControllerInfo();
|
||||
final State state = controllerInfo.getState();
|
||||
controllerInfo = receiveControllerInfo();
|
||||
State state = controllerInfo.getState();
|
||||
|
||||
OnOffType powerState = state.getOnOff();
|
||||
|
||||
@Nullable
|
||||
Ct colorTemperature = state.getColorTemperature();
|
||||
|
||||
float colorTempPercent = 0f;
|
||||
float colorTempPercent = 0.0F;
|
||||
int hue;
|
||||
int saturation;
|
||||
if (colorTemperature != null) {
|
||||
updateState(CHANNEL_COLOR_TEMPERATURE_ABS, new DecimalType(colorTemperature.getValue()));
|
||||
|
||||
@Nullable
|
||||
Integer min = colorTemperature.getMin();
|
||||
int colorMin = (min == null) ? 0 : min;
|
||||
|
||||
@Nullable
|
||||
hue = min == null ? 0 : min;
|
||||
Integer max = colorTemperature.getMax();
|
||||
int colorMax = (max == null) ? 0 : max;
|
||||
|
||||
colorTempPercent = (colorTemperature.getValue() - colorMin) / (colorMax - colorMin)
|
||||
* PercentType.HUNDRED.intValue();
|
||||
saturation = max == null ? 0 : max;
|
||||
colorTempPercent = (float) ((colorTemperature.getValue() - hue) / (saturation - hue)
|
||||
* PercentType.HUNDRED.intValue());
|
||||
}
|
||||
|
||||
updateState(CHANNEL_COLOR_TEMPERATURE, new PercentType(Float.toString(colorTempPercent)));
|
||||
updateState(CHANNEL_EFFECT, new StringType(controllerInfo.getEffects().getSelect()));
|
||||
|
||||
@Nullable
|
||||
Hue stateHue = state.getHue();
|
||||
int hue = (stateHue != null) ? stateHue.getValue() : 0;
|
||||
@Nullable
|
||||
hue = stateHue != null ? stateHue.getValue() : 0;
|
||||
|
||||
Sat stateSaturation = state.getSaturation();
|
||||
int saturation = (stateSaturation != null) ? stateSaturation.getValue() : 0;
|
||||
@Nullable
|
||||
saturation = stateSaturation != null ? stateSaturation.getValue() : 0;
|
||||
|
||||
Brightness stateBrightness = state.getBrightness();
|
||||
int brightness = (stateBrightness != null) ? stateBrightness.getValue() : 0;
|
||||
int brightness = stateBrightness != null ? stateBrightness.getValue() : 0;
|
||||
|
||||
updateState(CHANNEL_COLOR, new HSBType(new DecimalType(hue), new PercentType(saturation),
|
||||
new PercentType(powerState == OnOffType.ON ? brightness : 0)));
|
||||
@@ -582,9 +657,7 @@ public class NanoleafControllerHandler extends BaseBridgeHandler {
|
||||
properties.put(Thing.PROPERTY_MODEL_ID, controllerInfo.getModel());
|
||||
properties.put(Thing.PROPERTY_VENDOR, controllerInfo.getManufacturer());
|
||||
updateProperties(properties);
|
||||
|
||||
Configuration config = editConfiguration();
|
||||
|
||||
if (hasTouchSupport(controllerInfo.getModel())) {
|
||||
config.put(NanoleafControllerConfig.DEVICE_TYPE, DEVICE_TYPE_TOUCHSUPPORT);
|
||||
logger.debug("Set to device type {}", DEVICE_TYPE_TOUCHSUPPORT);
|
||||
@@ -603,7 +676,7 @@ public class NanoleafControllerHandler extends BaseBridgeHandler {
|
||||
});
|
||||
|
||||
// update the color channels of each panel
|
||||
this.getThing().getThings().forEach(child -> {
|
||||
getThing().getThings().forEach(child -> {
|
||||
NanoleafPanelHandler panelHandler = (NanoleafPanelHandler) child.getHandler();
|
||||
if (panelHandler != null) {
|
||||
logger.debug("Update color channel for panel {}", panelHandler.getThing().getUID());
|
||||
@@ -653,8 +726,8 @@ public class NanoleafControllerHandler extends BaseBridgeHandler {
|
||||
if (controllerInfo != null) {
|
||||
@Nullable
|
||||
Brightness brightness = controllerInfo.getState().getBrightness();
|
||||
int brightnessMin = 0;
|
||||
int brightnessMax = 0;
|
||||
int brightnessMin;
|
||||
int brightnessMax;
|
||||
if (brightness != null) {
|
||||
@Nullable
|
||||
Integer min = brightness.getMin();
|
||||
@@ -679,7 +752,7 @@ public class NanoleafControllerHandler extends BaseBridgeHandler {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
logger.warn("Unhandled command type: {}", command.getClass().getName());
|
||||
logger.warn("Unhandled command {} with command type: {}", command, command.getClass().getName());
|
||||
return;
|
||||
}
|
||||
break;
|
||||
@@ -736,30 +809,28 @@ public class NanoleafControllerHandler extends BaseBridgeHandler {
|
||||
Effects effects = new Effects();
|
||||
if (command instanceof StringType) {
|
||||
effects.setSelect(command.toString());
|
||||
Request setNewEffectRequest = OpenAPIUtils.requestBuilder(httpClient, getControllerConfig(), API_EFFECT,
|
||||
HttpMethod.PUT);
|
||||
String content = gson.toJson(effects);
|
||||
logger.debug("sending effect command from controller {}: {}", getThing().getUID(), content);
|
||||
setNewEffectRequest.content(new StringContentProvider(content), "application/json");
|
||||
OpenAPIUtils.sendOpenAPIRequest(setNewEffectRequest);
|
||||
} else {
|
||||
logger.warn("Unhandled command type: {}", command.getClass().getName());
|
||||
return;
|
||||
}
|
||||
Request setNewEffectRequest = OpenAPIUtils.requestBuilder(httpClient, getControllerConfig(), API_EFFECT,
|
||||
HttpMethod.PUT);
|
||||
String content = gson.toJson(effects);
|
||||
logger.debug("sending effect command from controller {}: {}", getThing().getUID(), content);
|
||||
setNewEffectRequest.content(new StringContentProvider(content), "application/json");
|
||||
OpenAPIUtils.sendOpenAPIRequest(setNewEffectRequest);
|
||||
}
|
||||
|
||||
private void sendRhythmCommand(Command command) throws NanoleafException {
|
||||
Rhythm rhythm = new Rhythm();
|
||||
if (command instanceof DecimalType) {
|
||||
rhythm.setRhythmMode(((DecimalType) command).intValue());
|
||||
Request setNewRhythmRequest = OpenAPIUtils.requestBuilder(httpClient, getControllerConfig(),
|
||||
API_RHYTHM_MODE, HttpMethod.PUT);
|
||||
setNewRhythmRequest.content(new StringContentProvider(gson.toJson(rhythm)), "application/json");
|
||||
OpenAPIUtils.sendOpenAPIRequest(setNewRhythmRequest);
|
||||
} else {
|
||||
logger.warn("Unhandled command type: {}", command.getClass().getName());
|
||||
return;
|
||||
}
|
||||
Request setNewRhythmRequest = OpenAPIUtils.requestBuilder(httpClient, getControllerConfig(), API_RHYTHM_MODE,
|
||||
HttpMethod.PUT);
|
||||
setNewRhythmRequest.content(new StringContentProvider(gson.toJson(rhythm)), "application/json");
|
||||
OpenAPIUtils.sendOpenAPIRequest(setNewRhythmRequest);
|
||||
}
|
||||
|
||||
private @Nullable String getAddress() {
|
||||
@@ -786,7 +857,8 @@ public class NanoleafControllerHandler extends BaseBridgeHandler {
|
||||
this.refreshIntervall = refreshIntervall;
|
||||
}
|
||||
|
||||
private @Nullable String getAuthToken() {
|
||||
@Nullable
|
||||
private String getAuthToken() {
|
||||
return authToken;
|
||||
}
|
||||
|
||||
@@ -794,7 +866,8 @@ public class NanoleafControllerHandler extends BaseBridgeHandler {
|
||||
this.authToken = authToken;
|
||||
}
|
||||
|
||||
private @Nullable String getDeviceType() {
|
||||
@Nullable
|
||||
private String getDeviceType() {
|
||||
return deviceType;
|
||||
}
|
||||
|
||||
|
||||
@@ -36,6 +36,7 @@ import org.openhab.binding.nanoleaf.internal.OpenAPIUtils;
|
||||
import org.openhab.binding.nanoleaf.internal.config.NanoleafControllerConfig;
|
||||
import org.openhab.binding.nanoleaf.internal.model.Effects;
|
||||
import org.openhab.binding.nanoleaf.internal.model.Write;
|
||||
import org.openhab.core.io.net.http.HttpClientFactory;
|
||||
import org.openhab.core.library.types.HSBType;
|
||||
import org.openhab.core.library.types.IncreaseDecreaseType;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
@@ -81,9 +82,9 @@ public class NanoleafPanelHandler extends BaseThingHandler {
|
||||
private @NonNullByDefault({}) ScheduledFuture<?> singleTapJob;
|
||||
private @NonNullByDefault({}) ScheduledFuture<?> doubleTapJob;
|
||||
|
||||
public NanoleafPanelHandler(Thing thing, HttpClient httpClient) {
|
||||
public NanoleafPanelHandler(Thing thing, HttpClientFactory httpClientFactory) {
|
||||
super(thing);
|
||||
this.httpClient = httpClient;
|
||||
this.httpClient = httpClientFactory.getCommonHttpClient();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
package org.openhab.binding.nanoleaf.internal.model;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
@@ -26,9 +25,9 @@ import com.google.gson.annotations.SerializedName;
|
||||
public class AuthToken {
|
||||
|
||||
@SerializedName("auth_token")
|
||||
private @Nullable String authToken;
|
||||
private String authToken = "";
|
||||
|
||||
public @Nullable String getAuthToken() {
|
||||
public String getAuthToken() {
|
||||
return authToken;
|
||||
}
|
||||
|
||||
|
||||
@@ -18,17 +18,21 @@ import java.util.TreeMap;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Represents layout of the light panels
|
||||
*
|
||||
* @author Martin Raepple - Initial contribution
|
||||
* @author Stefan Höhn - further improvements
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class Layout {
|
||||
|
||||
private int numPanels;
|
||||
private int sideLength;
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(Layout.class);
|
||||
|
||||
private @Nullable List<PositionDatum> positionData = null;
|
||||
|
||||
@@ -40,14 +44,6 @@ public class Layout {
|
||||
this.numPanels = numPanels;
|
||||
}
|
||||
|
||||
public int getSideLength() {
|
||||
return sideLength;
|
||||
}
|
||||
|
||||
public void setSideLength(int sideLength) {
|
||||
this.sideLength = sideLength;
|
||||
}
|
||||
|
||||
public @Nullable List<PositionDatum> getPositionData() {
|
||||
return positionData;
|
||||
}
|
||||
@@ -64,38 +60,46 @@ public class Layout {
|
||||
* @return a String containing the layout
|
||||
*/
|
||||
public String getLayoutView() {
|
||||
if (positionData != null) {
|
||||
List<PositionDatum> localPositionData = positionData;
|
||||
if (localPositionData != null) {
|
||||
String view = "";
|
||||
|
||||
int minx = Integer.MAX_VALUE;
|
||||
int maxx = Integer.MIN_VALUE;
|
||||
int miny = Integer.MAX_VALUE;
|
||||
int maxy = Integer.MIN_VALUE;
|
||||
int sideLength = Integer.MIN_VALUE;
|
||||
|
||||
final int noofDefinedPanels = positionData.size();
|
||||
final int noofDefinedPanels = localPositionData.size();
|
||||
|
||||
/*
|
||||
* Since 5.0.0 sidelengths are panelspecific and not delivered per layout but only the individual panel.
|
||||
* The only approximation we can do then is to derive the max-sidelength
|
||||
* the other issue is that panel sidelength have become fix per paneltype which has to be retrieved in a
|
||||
* hardcoded way.
|
||||
*/
|
||||
for (int index = 0; index < noofDefinedPanels; index++) {
|
||||
if (positionData != null) {
|
||||
@Nullable
|
||||
PositionDatum panel = positionData.get(index);
|
||||
PositionDatum panel = localPositionData.get(index);
|
||||
logger.debug("Layout: Panel position data x={} y={}", panel.getPosX(), panel.getPosY());
|
||||
|
||||
if (panel != null) {
|
||||
if (panel.getPosX() < minx) {
|
||||
minx = panel.getPosX();
|
||||
}
|
||||
if (panel.getPosX() > maxx) {
|
||||
maxx = panel.getPosX();
|
||||
}
|
||||
if (panel.getPosY() < miny) {
|
||||
miny = panel.getPosY();
|
||||
}
|
||||
if (panel.getPosY() > maxy) {
|
||||
maxy = panel.getPosY();
|
||||
}
|
||||
}
|
||||
if (panel.getPosX() < minx) {
|
||||
minx = panel.getPosX();
|
||||
}
|
||||
if (panel.getPosX() > maxx) {
|
||||
maxx = panel.getPosX();
|
||||
}
|
||||
if (panel.getPosY() < miny) {
|
||||
miny = panel.getPosY();
|
||||
}
|
||||
if (panel.getPosY() > maxy) {
|
||||
maxy = panel.getPosY();
|
||||
}
|
||||
if (panel.getPanelSize() > sideLength) {
|
||||
sideLength = panel.getPanelSize();
|
||||
}
|
||||
}
|
||||
|
||||
int shiftWidth = getSideLength() / 2;
|
||||
int shiftWidth = sideLength / 2;
|
||||
|
||||
if (shiftWidth == 0) {
|
||||
// seems we do not have squares here
|
||||
@@ -109,11 +113,10 @@ public class Layout {
|
||||
map = new TreeMap<>();
|
||||
for (int index = 0; index < noofDefinedPanels; index++) {
|
||||
|
||||
if (positionData != null) {
|
||||
@Nullable
|
||||
PositionDatum panel = positionData.get(index);
|
||||
if (localPositionData != null) {
|
||||
PositionDatum panel = localPositionData.get(index);
|
||||
|
||||
if (panel != null && panel.getPosY() == lineY) {
|
||||
if (panel.getPosY() == lineY) {
|
||||
map.put(panel.getPosX(), panel);
|
||||
}
|
||||
}
|
||||
@@ -121,9 +124,13 @@ public class Layout {
|
||||
lineY -= shiftWidth;
|
||||
for (int x = minx; x <= maxx; x += shiftWidth) {
|
||||
if (map.containsKey(x)) {
|
||||
@Nullable
|
||||
PositionDatum panel = map.get(x);
|
||||
view += String.format("%5s ", panel.getPanelId());
|
||||
if (panel != null) {
|
||||
int panelId = panel.getPanelId();
|
||||
view += String.format("%5s ", panelId);
|
||||
} else {
|
||||
view += " ";
|
||||
}
|
||||
} else {
|
||||
view += " ";
|
||||
}
|
||||
|
||||
@@ -12,6 +12,9 @@
|
||||
*/
|
||||
package org.openhab.binding.nanoleaf.internal.model;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
@@ -31,6 +34,25 @@ public class PositionDatum {
|
||||
private int posY;
|
||||
@SerializedName("o")
|
||||
private int orientation;
|
||||
@SerializedName("shapeType")
|
||||
private int shapeType;
|
||||
|
||||
private static Map<Integer, Integer> panelSizes = new HashMap<Integer, Integer>();
|
||||
|
||||
public PositionDatum() {
|
||||
// initialize constant sidelengths for panels. See https://forum.nanoleaf.me/docs chapter 3.3
|
||||
if (panelSizes.isEmpty()) {
|
||||
panelSizes.put(0, 150); // Triangle
|
||||
panelSizes.put(1, 0); // Rhythm N/A
|
||||
panelSizes.put(2, 100); // Square
|
||||
panelSizes.put(3, 100); // Control Square Master
|
||||
panelSizes.put(4, 100); // Control Square Passive
|
||||
panelSizes.put(7, 67); // Hexagon
|
||||
panelSizes.put(8, 134); // Triangle Shapes
|
||||
panelSizes.put(9, 67); // Mini Triangle Shapes
|
||||
panelSizes.put(12, 0); // Shapes Controller (N/A)
|
||||
}
|
||||
}
|
||||
|
||||
public int getPanelId() {
|
||||
return panelId;
|
||||
@@ -41,6 +63,9 @@ public class PositionDatum {
|
||||
}
|
||||
|
||||
public int getPosX() {
|
||||
if (getPanelSize() != 0 && posX % getPanelSize() == 99) { // hack: check the inaccuracy of 1
|
||||
posX = (posX / getPanelSize() + 1) * getPanelSize();
|
||||
}
|
||||
return posX;
|
||||
}
|
||||
|
||||
@@ -49,6 +74,13 @@ public class PositionDatum {
|
||||
}
|
||||
|
||||
public int getPosY() {
|
||||
// we need to fix the positions: see
|
||||
// https://forum.nanoleaf.me/forum/aurora-open-api/squares-send-unprecise-layout-positions
|
||||
// unfortunately this cannot be done in the setter as gson does not access setters
|
||||
|
||||
if (getPanelSize() != 0 && posY % getPanelSize() == 99) { // hack: check the inaccuracy of 1
|
||||
posY = (posY / getPanelSize() + 1) * getPanelSize();
|
||||
}
|
||||
return posY;
|
||||
}
|
||||
|
||||
@@ -63,4 +95,16 @@ public class PositionDatum {
|
||||
public void setOrientation(int o) {
|
||||
this.orientation = o;
|
||||
}
|
||||
|
||||
public int getShapeType() {
|
||||
return shapeType;
|
||||
}
|
||||
|
||||
public void setShapeType(int shapeType) {
|
||||
this.shapeType = shapeType;
|
||||
}
|
||||
|
||||
public Integer getPanelSize() {
|
||||
return panelSizes.getOrDefault(shapeType, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,7 +41,8 @@ public class State {
|
||||
}
|
||||
|
||||
public OnOffType getOnOff() {
|
||||
return (on != null && on.getValue()) ? OnOffType.ON : OnOffType.OFF;
|
||||
On localOn = on;
|
||||
return (localOn != null && localOn.getValue()) ? OnOffType.ON : OnOffType.OFF;
|
||||
}
|
||||
|
||||
public void setOn(On on) {
|
||||
|
||||
@@ -16,11 +16,13 @@ import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* Represents write command to set solid color effect
|
||||
*
|
||||
* @author Martin Raepple - Initial contribution
|
||||
* @author Stefan Höhn - Made colorType nullable
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class Write {
|
||||
@@ -29,7 +31,8 @@ public class Write {
|
||||
private String animType = "";
|
||||
private String animName = "";
|
||||
private List<Palette> palette = new ArrayList<>();
|
||||
private String colorType = "";
|
||||
@Nullable
|
||||
private String colorType; // is required to be null if not set!
|
||||
private String animData = "";
|
||||
private boolean loop = false;
|
||||
|
||||
@@ -57,7 +60,7 @@ public class Write {
|
||||
this.palette = palette;
|
||||
}
|
||||
|
||||
public String getColorType() {
|
||||
public @Nullable String getColorType() {
|
||||
return colorType;
|
||||
}
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
<default>lightPanels</default>
|
||||
<options>
|
||||
<option value="lightPanels">Light Panels</option>
|
||||
<option value="canvas">Canvas</option>
|
||||
<option value="canvas">Canvas/Shapes</option>
|
||||
</options>
|
||||
</parameter>
|
||||
</config-description>
|
||||
|
||||
@@ -36,6 +36,8 @@ channel-type.nanoleaf.panelColor.label = Panel Color
|
||||
channel-type.nanoleaf.panelColor.description = Color of the individual panel
|
||||
channel-type.nanoleaf.tap.label = Button
|
||||
channel-type.nanoleaf.tap.description = Button events of the panel
|
||||
channel-type.nanoleaf.swipe.label = Swipe
|
||||
channel-type.nanoleaf.swipe.description = Swipe over the panels
|
||||
|
||||
# error messages
|
||||
error.nanoleaf.controller.noIp = IP/host address and/or port are not configured for the controller.
|
||||
|
||||
@@ -36,6 +36,8 @@ channel-type.nanoleaf.panelColor.label = Paneelfarbe
|
||||
channel-type.nanoleaf.panelColor.description = Farbe des einzelnen Paneels
|
||||
channel-type.nanoleaf.tap.label = Taster
|
||||
channel-type.nanoleaf.tap.description = Tastevents des Panels
|
||||
channel-type.nanoleaf.swipe.label = Wischen (Swipe)
|
||||
channel-type.nanoleaf.swipe.description = Wischen (Swipe) über die Panels
|
||||
|
||||
# error messages
|
||||
error.nanoleaf.controller.noIp = IP/Host-Adresse und/oder Port sind für den Controller nicht konfiguriert.
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
<channel id="rhythmState" typeId="rhythmState"/>
|
||||
<channel id="rhythmActive" typeId="rhythmActive"/>
|
||||
<channel id="rhythmMode" typeId="rhythmMode"/>
|
||||
<channel id="swipe" typeId="swipe"/>
|
||||
</channels>
|
||||
|
||||
<properties>
|
||||
@@ -92,4 +93,18 @@
|
||||
</state>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="swipe">
|
||||
<kind>trigger</kind>
|
||||
<label>@text/channel-type.nanoleaf.swipe.label</label>
|
||||
<description>@text/channel-type.nanoleaf.swipe.description</description>
|
||||
<event>
|
||||
<options>
|
||||
<option value="UP">Up</option>
|
||||
<option value="DOWN">Down</option>
|
||||
<option value="LEFT">Left</option>
|
||||
<option value="RIGHT">Right</option>
|
||||
</options>
|
||||
</event>
|
||||
</channel-type>
|
||||
|
||||
</thing:thing-descriptions>
|
||||
|
||||
@@ -20,6 +20,7 @@ import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.openhab.binding.nanoleaf.internal.model.Layout;
|
||||
import org.openhab.binding.nanoleaf.internal.model.Write;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
|
||||
@@ -38,8 +39,36 @@ public class LayoutTest {
|
||||
|
||||
@BeforeEach
|
||||
public void setup() {
|
||||
layout1Json = "{\"numPanels\":14,\"sideLength\":100,\"positionData\":[{\"panelId\":41451,\"x\":350,\"y\":0,\"o\":0,\"shapeType\":3},{\"panelId\":8134,\"x\":350,\"y\":150,\"o\":0,\"shapeType\":2},{\"panelId\":58086,\"x\":200,\"y\":100,\"o\":270,\"shapeType\":2},{\"panelId\":38724,\"x\":300,\"y\":200,\"o\":0,\"shapeType\":2},{\"panelId\":48111,\"x\":200,\"y\":200,\"o\":270,\"shapeType\":2},{\"panelId\":56093,\"x\":100,\"y\":200,\"o\":0,\"shapeType\":2},{\"panelId\":55836,\"x\":0,\"y\":200,\"o\":0,\"shapeType\":2},{\"panelId\":31413,\"x\":100,\"y\":300,\"o\":90,\"shapeType\":2},{\"panelId\":9162,\"x\":300,\"y\":300,\"o\":90,\"shapeType\":2},{\"panelId\":13276,\"x\":400,\"y\":300,\"o\":90,\"shapeType\":2},{\"panelId\":17870,\"x\":400,\"y\":200,\"o\":0,\"shapeType\":2},{\"panelId\":5164,\"x\":500,\"y\":200,\"o\":0,\"shapeType\":2},{\"panelId\":64279,\"x\":600,\"y\":200,\"o\":0,\"shapeType\":2},{\"panelId\":39755,\"x\":500,\"y\":100,\"o\":90,\"shapeType\":2}]}";
|
||||
// panel number is not consistent to returned panels in array but it should still work
|
||||
layout1Json = "{\n" + " \"numPanels\": 14,\n" + " \"sideLength\": 0,\n"
|
||||
+ " \"positionData\": [\n" + " {\n" + " \"panelId\": 60147,\n"
|
||||
+ " \"x\": 199,\n" + " \"y\": 99,\n" + " \"o\": 0,\n"
|
||||
+ " \"shapeType\": 3\n" + " },\n" + " {\n" + " \"panelId\": 61141,\n"
|
||||
+ " \"x\": 200,\n" + " \"y\": 199,\n" + " \"o\": 90,\n"
|
||||
+ " \"shapeType\": 2\n" + " },\n" + " {\n" + " \"panelId\": 42064,\n"
|
||||
+ " \"x\": 100,\n" + " \"y\": 200,\n" + " \"o\": 180,\n"
|
||||
+ " \"shapeType\": 2\n" + " },\n" + " {\n" + " \"panelId\": 186,\n"
|
||||
+ " \"x\": 0,\n" + " \"y\": 200,\n" + " \"o\": 180,\n"
|
||||
+ " \"shapeType\": 2\n" + " },\n" + " {\n" + " \"panelId\": 19209,\n"
|
||||
+ " \"x\": 0,\n" + " \"y\": 100,\n" + " \"o\": 270,\n"
|
||||
+ " \"shapeType\": 2\n" + " },\n" + " {\n" + " \"panelId\": 36604,\n"
|
||||
+ " \"x\": 300,\n" + " \"y\": 99,\n" + " \"o\": 0,\n"
|
||||
+ " \"shapeType\": 2\n" + " },\n" + " {\n" + " \"panelId\": 37121,\n"
|
||||
+ " \"x\": 400,\n" + " \"y\": 99,\n" + " \"o\": 270,\n"
|
||||
+ " \"shapeType\": 2\n" + " },\n" + " {\n" + " \"panelId\": 45187,\n"
|
||||
+ " \"x\": 400,\n" + " \"y\": 199,\n" + " \"o\": 270,\n"
|
||||
+ " \"shapeType\": 2\n" + " },\n" + " {\n" + " \"panelId\": 33626,\n"
|
||||
+ " \"x\": 500,\n" + " \"y\": 199,\n" + " \"o\": 270,\n"
|
||||
+ " \"shapeType\": 2\n" + " },\n" + " {\n" + " \"panelId\": 10523,\n"
|
||||
+ " \"x\": 600,\n" + " \"y\": 199,\n" + " \"o\": 270,\n"
|
||||
+ " \"shapeType\": 2\n" + " },\n" + " {\n" + " \"panelId\": 54086,\n"
|
||||
+ " \"x\": 599,\n" + " \"y\": 99,\n" + " \"o\": 540,\n"
|
||||
+ " \"shapeType\": 2\n" + " },\n" + " {\n" + " \"panelId\": 3512,\n"
|
||||
+ " \"x\": 699,\n" + " \"y\": 99,\n" + " \"o\": 540,\n"
|
||||
+ " \"shapeType\": 2\n" + " },\n" + " {\n" + " \"panelId\": 16398,\n"
|
||||
+ " \"x\": 799,\n" + " \"y\": 99,\n" + " \"o\": 540,\n"
|
||||
+ " \"shapeType\": 2\n" + " },\n" + " {\n" + " \"panelId\": 39163,\n"
|
||||
+ " \"x\": 800,\n" + " \"y\": 199,\n" + " \"o\": 630,\n"
|
||||
+ " \"shapeType\": 2\n" + " }\n" + " ]\n" + " }";
|
||||
layoutInconsistentPanelNoJson = "{\"numPanels\":15,\"sideLength\":100,\"positionData\":[{\"panelId\":41451,\"x\":350,\"y\":0,\"o\":0,\"shapeType\":3},{\"panelId\":8134,\"x\":350,\"y\":150,\"o\":0,\"shapeType\":2},{\"panelId\":58086,\"x\":200,\"y\":100,\"o\":270,\"shapeType\":2},{\"panelId\":38724,\"x\":300,\"y\":200,\"o\":0,\"shapeType\":2},{\"panelId\":48111,\"x\":200,\"y\":200,\"o\":270,\"shapeType\":2},{\"panelId\":56093,\"x\":100,\"y\":200,\"o\":0,\"shapeType\":2},{\"panelId\":55836,\"x\":0,\"y\":200,\"o\":0,\"shapeType\":2},{\"panelId\":31413,\"x\":100,\"y\":300,\"o\":90,\"shapeType\":2},{\"panelId\":9162,\"x\":300,\"y\":300,\"o\":90,\"shapeType\":2},{\"panelId\":13276,\"x\":400,\"y\":300,\"o\":90,\"shapeType\":2},{\"panelId\":17870,\"x\":400,\"y\":200,\"o\":0,\"shapeType\":2},{\"panelId\":5164,\"x\":500,\"y\":200,\"o\":0,\"shapeType\":2},{\"panelId\":64279,\"x\":600,\"y\":200,\"o\":0,\"shapeType\":2},{\"panelId\":39755,\"x\":500,\"y\":100,\"o\":90,\"shapeType\":2}]}";
|
||||
}
|
||||
|
||||
@@ -47,6 +76,23 @@ public class LayoutTest {
|
||||
public void testTheRightLayoutView() {
|
||||
@Nullable
|
||||
Layout layout = gson.fromJson(layout1Json, Layout.class);
|
||||
if (layout == null) {
|
||||
layout = new Layout();
|
||||
}
|
||||
String layoutView = layout.getLayoutView();
|
||||
assertThat(layoutView, is(equalTo(
|
||||
" 186 42064 61141 45187 33626 10523 39163 \n"
|
||||
+ " \n"
|
||||
+ "19209 60147 36604 37121 54086 3512 16398 \n")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTheInconsistentLayoutView() {
|
||||
@Nullable
|
||||
Layout layout = gson.fromJson(layoutInconsistentPanelNoJson, Layout.class);
|
||||
if (layout == null) {
|
||||
layout = new Layout();
|
||||
}
|
||||
String layoutView = layout.getLayoutView();
|
||||
assertThat(layoutView,
|
||||
is(equalTo(" 31413 9162 13276 \n"
|
||||
@@ -59,17 +105,17 @@ public class LayoutTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTheInconsistentLayoutView() {
|
||||
@Nullable
|
||||
Layout layout = gson.fromJson(layoutInconsistentPanelNoJson, Layout.class);
|
||||
String layoutView = layout.getLayoutView();
|
||||
assertThat(layoutView,
|
||||
is(equalTo(" 31413 9162 13276 \n"
|
||||
+ " \n"
|
||||
+ "55836 56093 48111 38724 17870 5164 64279 \n"
|
||||
+ " 8134 \n"
|
||||
+ " 58086 39755 \n"
|
||||
+ " \n"
|
||||
+ " 41451 \n")));
|
||||
public void testEffects() {
|
||||
Write write = new Write();
|
||||
write.setCommand("display");
|
||||
write.setAnimType("static");
|
||||
write.setLoop(false);
|
||||
int panelID = 123;
|
||||
int quotient = Integer.divideUnsigned(Integer.valueOf(panelID), 256);
|
||||
int remainder = Integer.remainderUnsigned(Integer.valueOf(panelID), 256);
|
||||
write.setAnimData(String.format("0 1 %d %d %d %d %d 0 0 10", quotient, remainder, 20, 40, 60));
|
||||
String content = gson.toJson(write);
|
||||
assertThat(content, containsStringIgnoringCase("palette"));
|
||||
assertThat(content, is(not(containsStringIgnoringCase("colorType"))));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ import org.junit.jupiter.api.Test;
|
||||
*/
|
||||
|
||||
@NonNullByDefault
|
||||
public class OpenAPUUtilsTest {
|
||||
public class OpenAPIUtilsTest {
|
||||
|
||||
@Test
|
||||
public void testStateOn() {
|
||||
@@ -16,6 +16,8 @@ import static org.hamcrest.CoreMatchers.is;
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.greaterThan;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -38,12 +40,16 @@ public class TouchTest {
|
||||
@Test
|
||||
public void testTheRightLayoutView() {
|
||||
String json = "{\"events\":[{\"panelId\":48111,\"gesture\":1}]}";
|
||||
@Nullable
|
||||
|
||||
TouchEvents touchEvents = gson.fromJson(json, TouchEvents.class);
|
||||
assertThat(touchEvents.getEvents().size(), greaterThan(0));
|
||||
assertThat(touchEvents.getEvents().size(), is(1));
|
||||
if (touchEvents == null) {
|
||||
touchEvents = new TouchEvents();
|
||||
}
|
||||
List<TouchEvent> events = touchEvents.getEvents();
|
||||
assertThat(events.size(), greaterThan(0));
|
||||
assertThat(events.size(), is(1));
|
||||
@Nullable
|
||||
TouchEvent touchEvent = touchEvents.getEvents().get(0);
|
||||
TouchEvent touchEvent = events.get(0);
|
||||
assertThat(touchEvent.getPanelId(), is("48111"));
|
||||
assertThat(touchEvent.getGesture(), is(1));
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import static org.hamcrest.CoreMatchers.*;
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.openhab.binding.nanoleaf.internal.model.ControllerInfo;
|
||||
@@ -45,12 +46,15 @@ public class NanoleafControllerHandlerTest {
|
||||
public void testStateOn() {
|
||||
controllerInfoJSON = "{\r\n \"name\":\"Nanoleaf Light Panels 12:34:56\",\r\n \"serialNo\":\"S19082ABCDE\",\r\n \"manufacturer\":\"Nanoleaf\",\r\n \"firmwareVersion\":\"3.3.3\",\r\n \"hardwareVersion\":\"1.6-2\",\r\n \"model\":\"NL22\",\r\n \"cloudHash\":{\r\n\r\n },\r\n \"discovery\":{\r\n\r\n },\r\n \"effects\":{\r\n \"effectsList\":[\r\n \"Color Burst\",\r\n \"Fireworks\",\r\n \"Flames\",\r\n \"Forest\",\r\n \"Inner Peace\",\r\n \"Lightning\",\r\n \"Northern Lights\",\r\n \"Pulse Pop Beats\",\r\n \"Vibrant Sunrise\"\r\n ],\r\n \"select\":\"Flames\"\r\n },\r\n \"firmwareUpgrade\":{\r\n\r\n },\r\n \"panelLayout\":{\r\n \"globalOrientation\":{\r\n \"value\":0,\r\n \"max\":360,\r\n \"min\":0\r\n },\r\n \"layout\":{\r\n \"numPanels\":9,\r\n \"sideLength\":150,\r\n \"positionData\":[\r\n {\r\n \"panelId\":1,\r\n \"x\":299,\r\n \"y\":0,\r\n \"o\":300,\r\n \"shapeType\":0\r\n },\r\n {\r\n \"panelId\":2,\r\n \"x\":299,\r\n \"y\":86,\r\n \"o\":120,\r\n \"shapeType\":0\r\n },\r\n {\r\n \"panelId\":3,\r\n \"x\":224,\r\n \"y\":129,\r\n \"o\":60,\r\n \"shapeType\":0\r\n },\r\n {\r\n \"panelId\":4,\r\n \"x\":224,\r\n \"y\":216,\r\n \"o\":120,\r\n \"shapeType\":0\r\n },\r\n {\r\n \"panelId\":5,\r\n \"x\":149,\r\n \"y\":259,\r\n \"o\":60,\r\n \"shapeType\":0\r\n },\r\n {\r\n \"panelId\":6,\r\n \"x\":74,\r\n \"y\":216,\r\n \"o\":240,\r\n \"shapeType\":0\r\n },\r\n {\r\n \"panelId\":7,\r\n \"x\":0,\r\n \"y\":259,\r\n \"o\":60,\r\n \"shapeType\":0\r\n },\r\n {\r\n \"panelId\":8,\r\n \"x\":149,\r\n \"y\":346,\r\n \"o\":120,\r\n \"shapeType\":0\r\n },\r\n {\r\n \"panelId\":9,\r\n \"x\":374,\r\n \"y\":129,\r\n \"o\":180,\r\n \"shapeType\":0\r\n }\r\n ]\r\n }\r\n },\r\n \"rhythm\":{\r\n \"auxAvailable\":false,\r\n \"firmwareVersion\":\"2.4.3\",\r\n \"hardwareVersion\":\"2.0\",\r\n \"rhythmActive\":false,\r\n \"rhythmConnected\":true,\r\n \"rhythmId\":10,\r\n \"rhythmMode\":0,\r\n \"rhythmPos\":{\r\n \"x\":449.99521692839559,\r\n \"y\":86.60030339609753,\r\n \"o\":0.0\r\n }\r\n },\r\n \"schedules\":{\r\n\r\n },\r\n \"state\":{\r\n \"brightness\":{\r\n \"value\":29,\r\n \"max\":100,\r\n \"min\":0\r\n },\r\n \"colorMode\":\"effect\",\r\n \"ct\":{\r\n \"value\":3000,\r\n \"max\":6500,\r\n \"min\":1200\r\n },\r\n \"hue\":{\r\n \"value\":0,\r\n \"max\":360,\r\n \"min\":0\r\n },\r\n \"on\":{\r\n \"value\":true\r\n },\r\n \"sat\":{\r\n \"value\":0,\r\n \"max\":100,\r\n \"min\":0\r\n }\r\n }\r\n}";
|
||||
|
||||
@Nullable
|
||||
ControllerInfo controllerInfo = gson.fromJson(controllerInfoJSON, ControllerInfo.class);
|
||||
assertThat(controllerInfo, is(notNullValue()));
|
||||
|
||||
final State state = controllerInfo.getState();
|
||||
assertThat(state, is(notNullValue()));
|
||||
|
||||
assertThat(state.getOnOff(), is(OnOffType.ON));
|
||||
if (controllerInfo != null) {
|
||||
final State state = controllerInfo.getState();
|
||||
assertThat(state, is(notNullValue()));
|
||||
assertThat(state.getOnOff(), is(OnOffType.ON));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -58,11 +62,13 @@ public class NanoleafControllerHandlerTest {
|
||||
controllerInfoJSON = "{\r\n \"name\":\"Nanoleaf Light Panels 12:34:56\",\r\n \"serialNo\":\"S19082ABCDE\",\r\n \"manufacturer\":\"Nanoleaf\",\r\n \"firmwareVersion\":\"3.3.3\",\r\n \"hardwareVersion\":\"1.6-2\",\r\n \"model\":\"NL22\",\r\n \"cloudHash\":{\r\n\r\n },\r\n \"discovery\":{\r\n\r\n },\r\n \"effects\":{\r\n \"effectsList\":[\r\n \"Color Burst\",\r\n \"Fireworks\",\r\n \"Flames\",\r\n \"Forest\",\r\n \"Inner Peace\",\r\n \"Lightning\",\r\n \"Northern Lights\",\r\n \"Pulse Pop Beats\",\r\n \"Vibrant Sunrise\"\r\n ],\r\n \"select\":\"Flames\"\r\n },\r\n \"firmwareUpgrade\":{\r\n\r\n },\r\n \"panelLayout\":{\r\n \"globalOrientation\":{\r\n \"value\":0,\r\n \"max\":360,\r\n \"min\":0\r\n },\r\n \"layout\":{\r\n \"numPanels\":9,\r\n \"sideLength\":150,\r\n \"positionData\":[\r\n {\r\n \"panelId\":1,\r\n \"x\":299,\r\n \"y\":0,\r\n \"o\":300,\r\n \"shapeType\":0\r\n },\r\n {\r\n \"panelId\":2,\r\n \"x\":299,\r\n \"y\":86,\r\n \"o\":120,\r\n \"shapeType\":0\r\n },\r\n {\r\n \"panelId\":3,\r\n \"x\":224,\r\n \"y\":129,\r\n \"o\":60,\r\n \"shapeType\":0\r\n },\r\n {\r\n \"panelId\":4,\r\n \"x\":224,\r\n \"y\":216,\r\n \"o\":120,\r\n \"shapeType\":0\r\n },\r\n {\r\n \"panelId\":5,\r\n \"x\":149,\r\n \"y\":259,\r\n \"o\":60,\r\n \"shapeType\":0\r\n },\r\n {\r\n \"panelId\":6,\r\n \"x\":74,\r\n \"y\":216,\r\n \"o\":240,\r\n \"shapeType\":0\r\n },\r\n {\r\n \"panelId\":7,\r\n \"x\":0,\r\n \"y\":259,\r\n \"o\":60,\r\n \"shapeType\":0\r\n },\r\n {\r\n \"panelId\":8,\r\n \"x\":149,\r\n \"y\":346,\r\n \"o\":120,\r\n \"shapeType\":0\r\n },\r\n {\r\n \"panelId\":9,\r\n \"x\":374,\r\n \"y\":129,\r\n \"o\":180,\r\n \"shapeType\":0\r\n }\r\n ]\r\n }\r\n },\r\n \"rhythm\":{\r\n \"auxAvailable\":false,\r\n \"firmwareVersion\":\"2.4.3\",\r\n \"hardwareVersion\":\"2.0\",\r\n \"rhythmActive\":false,\r\n \"rhythmConnected\":true,\r\n \"rhythmId\":10,\r\n \"rhythmMode\":0,\r\n \"rhythmPos\":{\r\n \"x\":449.99521692839559,\r\n \"y\":86.60030339609753,\r\n \"o\":0.0\r\n }\r\n },\r\n \"schedules\":{\r\n\r\n },\r\n \"state\":{\r\n \"brightness\":{\r\n \"value\":29,\r\n \"max\":100,\r\n \"min\":0\r\n },\r\n \"colorMode\":\"effect\",\r\n \"ct\":{\r\n \"value\":3000,\r\n \"max\":6500,\r\n \"min\":1200\r\n },\r\n \"hue\":{\r\n \"value\":0,\r\n \"max\":360,\r\n \"min\":0\r\n },\r\n \"sat\":{\r\n \"value\":0,\r\n \"max\":100,\r\n \"min\":0\r\n }\r\n }\r\n}";
|
||||
|
||||
ControllerInfo controllerInfo = gson.fromJson(controllerInfoJSON, ControllerInfo.class);
|
||||
assertThat(controllerInfo, is(notNullValue()));
|
||||
|
||||
final State state = controllerInfo.getState();
|
||||
assertThat(state, is(notNullValue()));
|
||||
|
||||
assertThat(state.getOnOff(), is(OnOffType.OFF));
|
||||
if (controllerInfo != null) {
|
||||
final State state = controllerInfo.getState();
|
||||
assertThat(state, is(notNullValue()));
|
||||
assertThat(state.getOnOff(), is(OnOffType.OFF));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -70,10 +76,12 @@ public class NanoleafControllerHandlerTest {
|
||||
controllerInfoJSON = "{\r\n \"name\":\"Nanoleaf Light Panels 12:34:56\",\r\n \"serialNo\":\"S19082ABCDE\",\r\n \"manufacturer\":\"Nanoleaf\",\r\n \"firmwareVersion\":\"3.3.3\",\r\n \"hardwareVersion\":\"1.6-2\",\r\n \"model\":\"NL22\",\r\n \"cloudHash\":{\r\n\r\n },\r\n \"discovery\":{\r\n\r\n },\r\n \"effects\":{\r\n \"effectsList\":[\r\n \"Color Burst\",\r\n \"Fireworks\",\r\n \"Flames\",\r\n \"Forest\",\r\n \"Inner Peace\",\r\n \"Lightning\",\r\n \"Northern Lights\",\r\n \"Pulse Pop Beats\",\r\n \"Vibrant Sunrise\"\r\n ],\r\n \"select\":\"Flames\"\r\n },\r\n \"firmwareUpgrade\":{\r\n\r\n },\r\n \"panelLayout\":{\r\n \"globalOrientation\":{\r\n \"value\":0,\r\n \"max\":360,\r\n \"min\":0\r\n },\r\n \"layout\":{\r\n \"numPanels\":9,\r\n \"sideLength\":150,\r\n \"positionData\":[\r\n {\r\n \"panelId\":1,\r\n \"x\":299,\r\n \"y\":0,\r\n \"o\":300,\r\n \"shapeType\":0\r\n },\r\n {\r\n \"panelId\":2,\r\n \"x\":299,\r\n \"y\":86,\r\n \"o\":120,\r\n \"shapeType\":0\r\n },\r\n {\r\n \"panelId\":3,\r\n \"x\":224,\r\n \"y\":129,\r\n \"o\":60,\r\n \"shapeType\":0\r\n },\r\n {\r\n \"panelId\":4,\r\n \"x\":224,\r\n \"y\":216,\r\n \"o\":120,\r\n \"shapeType\":0\r\n },\r\n {\r\n \"panelId\":5,\r\n \"x\":149,\r\n \"y\":259,\r\n \"o\":60,\r\n \"shapeType\":0\r\n },\r\n {\r\n \"panelId\":6,\r\n \"x\":74,\r\n \"y\":216,\r\n \"o\":240,\r\n \"shapeType\":0\r\n },\r\n {\r\n \"panelId\":7,\r\n \"x\":0,\r\n \"y\":259,\r\n \"o\":60,\r\n \"shapeType\":0\r\n },\r\n {\r\n \"panelId\":8,\r\n \"x\":149,\r\n \"y\":346,\r\n \"o\":120,\r\n \"shapeType\":0\r\n },\r\n {\r\n \"panelId\":9,\r\n \"x\":374,\r\n \"y\":129,\r\n \"o\":180,\r\n \"shapeType\":0\r\n }\r\n ]\r\n }\r\n },\r\n \"rhythm\":{\r\n \"auxAvailable\":false,\r\n \"firmwareVersion\":\"2.4.3\",\r\n \"hardwareVersion\":\"2.0\",\r\n \"rhythmActive\":false,\r\n \"rhythmConnected\":true,\r\n \"rhythmId\":10,\r\n \"rhythmMode\":0,\r\n \"rhythmPos\":{\r\n \"x\":449.99521692839559,\r\n \"y\":86.60030339609753,\r\n \"o\":0.0\r\n }\r\n },\r\n \"schedules\":{\r\n\r\n },\r\n \"state\":{\r\n \"brightness\":{\r\n \"value\":29,\r\n \"max\":100,\r\n \"min\":0\r\n },\r\n \"colorMode\":\"effect\",\r\n \"ct\":{\r\n \"value\":3000,\r\n \"max\":6500,\r\n \"min\":1200\r\n },\r\n \"hue\":{\r\n \"value\":0,\r\n \"max\":360,\r\n \"min\":0\r\n },\r\n \"on\":{\r\n \"value\":false\r\n },\r\n \"sat\":{\r\n \"value\":0,\r\n \"max\":100,\r\n \"min\":0\r\n }\r\n }\r\n}";
|
||||
|
||||
ControllerInfo controllerInfo = gson.fromJson(controllerInfoJSON, ControllerInfo.class);
|
||||
assertThat(controllerInfo, is(notNullValue()));
|
||||
|
||||
final State state = controllerInfo.getState();
|
||||
assertThat(state, is(notNullValue()));
|
||||
|
||||
assertThat(state.getOnOff(), is(OnOffType.OFF));
|
||||
if (controllerInfo != null) {
|
||||
final State state = controllerInfo.getState();
|
||||
assertThat(state, is(notNullValue()));
|
||||
assertThat(state.getOnOff(), is(OnOffType.OFF));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user