added migrated 2.x add-ons

Signed-off-by: Kai Kreuzer <kai@openhab.org>
This commit is contained in:
Kai Kreuzer
2020-09-21 01:58:32 +02:00
parent bbf1a7fd29
commit 6df6783b60
11662 changed files with 1302875 additions and 11 deletions

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<features name="org.openhab.binding.nanoleaf-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
<repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
<feature name="openhab-binding-nanoleaf" description="Nanoleaf Binding" version="${project.version}">
<feature>openhab-runtime-base</feature>
<feature>openhab-transport-mdns</feature>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.nanoleaf/${project.version}</bundle>
</feature>
</features>

View File

@@ -0,0 +1,30 @@
/**
* Copyright (c) 2010-2020 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.nanoleaf.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Exception if request to Nanoleaf OpenAPI does not expect the given content
*
* @author Stefan Höhn - Initial contribution
*/
@NonNullByDefault
public class NanoleafBadRequestException extends NanoleafException {
private static final long serialVersionUID = -6941678941424573256L;
public NanoleafBadRequestException(String message) {
super(message);
}
}

View File

@@ -0,0 +1,87 @@
/**
* Copyright (c) 2010-2020 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.nanoleaf.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.ThingTypeUID;
/**
* The {@link NanoleafBindingConstants} class defines common constants, which are
* used across the whole binding.
*
* @author Martin Raepple - Initial contribution
*/
@NonNullByDefault
public class NanoleafBindingConstants {
private static final String BINDING_ID = "nanoleaf";
// List of all Thing Type UIDs
public static final ThingTypeUID THING_TYPE_CONTROLLER = new ThingTypeUID(BINDING_ID, "controller");
public static final ThingTypeUID THING_TYPE_LIGHT_PANEL = new ThingTypeUID(BINDING_ID, "lightpanel");
// Controller configuration settings
public static final String CONFIG_ADDRESS = "address";
public static final String CONFIG_PORT = "port";
public static final String CONFIG_AUTH_TOKEN = "authToken";
public static final String CONFIG_DEVICE_TYPE_CANVAS = "canvas";
public static final String CONFIG_DEVICE_TYPE_LIGHTPANELS = "lightPanels";
// Panel configuration settings
public static final String CONFIG_PANEL_ID = "id";
// List of controller channels
public static final String CHANNEL_POWER = "power";
public static final String CHANNEL_COLOR = "color";
public static final String CHANNEL_COLOR_TEMPERATURE = "colorTemperature";
public static final String CHANNEL_COLOR_TEMPERATURE_ABS = "colorTemperatureAbs";
public static final String CHANNEL_COLOR_MODE = "colorMode";
public static final String CHANNEL_EFFECT = "effect";
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_PANEL_LAYOUT = "panelLayout";
// List of light panel channels
public static final String CHANNEL_PANEL_COLOR = "panelColor";
public static final String CHANNEL_PANEL_SINGLE_TAP = "singleTap";
public static final String CHANNEL_PANEL_DOUBLE_TAP = "doubleTap";
// Nanoleaf OpenAPI URLs
public static final String API_V1_BASE_URL = "/api/v1";
public static final String API_GET_CONTROLLER_INFO = "/";
public static final String API_ADD_USER = "/new";
public static final String API_EVENTS = "/events";
public static final String API_DELETE_USER = "";
public static final String API_SET_VALUE = "/state";
public static final String API_EFFECT = "/effects";
public static final String API_RHYTHM_MODE = "/rhythm/rhythmMode";
// Nanoleaf model IDs and minimum required firmware versions
public static final String API_MIN_FW_VER_LIGHTPANELS = "1.5.0";
public static final String API_MIN_FW_VER_CANVAS = "1.1.0";
public static final String MODEL_ID_LIGHTPANELS = "NL22";
public static final String MODEL_ID_CANVAS = "NL29";
public static final String DEVICE_TYPE_LIGHTPANELS = "lightPanels";
public static final String DEVICE_TYPE_CANVAS = "canvas";
// mDNS discovery service type
// see http://forum.nanoleaf.me/docs/openapi#_gf9l5guxt8r0
public static final String SERVICE_TYPE = "_nanoleafapi._tcp.local.";
// Effect/scene name for static color
public static final String EFFECT_NAME_STATIC_COLOR = "*Dynamic*";
// Color channels increase/decrease brightness step size
public static final int BRIGHTNESS_STEP_SIZE = 5;
}

View File

@@ -0,0 +1,36 @@
/**
* Copyright (c) 2010-2020 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.nanoleaf.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.nanoleaf.internal.model.ControllerInfo;
import org.openhab.core.thing.ThingUID;
/**
* A {@link NanoleafControllerListener} is notified by the controller thing handler.
* A listener may use it to discover additional things connected to the controller (bridge), such as individual panels.
*
* @author Martin Raepple - Initial contribution
*/
@NonNullByDefault
public interface NanoleafControllerListener {
/**
* This method is called after the bridge thing handler fetched the controller info
*
* @param bridge the Nanoleaf controller.
* @param controllerInfo the controller data with panel information
*/
void onControllerInfoFetched(ThingUID bridge, ControllerInfo controllerInfo);
}

View File

@@ -0,0 +1,37 @@
/**
* Copyright (c) 2010-2020 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.nanoleaf.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* General binding exception if something goes wrong.
*
* @author Martin Raepple - Initial contribution
*/
@NonNullByDefault
public class NanoleafException extends Exception {
private static final long serialVersionUID = 1L;
public NanoleafException(String message) {
super(message);
}
public NanoleafException(final Throwable cause) {
super(cause);
}
public NanoleafException(final String message, final Throwable cause) {
super(message, cause);
}
}

View File

@@ -0,0 +1,116 @@
/**
* Copyright (c) 2010-2020 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.nanoleaf.internal;
import static org.openhab.binding.nanoleaf.internal.NanoleafBindingConstants.*;
import java.util.Collections;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
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.discovery.NanoleafPanelsDiscoveryService;
import org.openhab.binding.nanoleaf.internal.handler.NanoleafControllerHandler;
import org.openhab.binding.nanoleaf.internal.handler.NanoleafPanelHandler;
import org.openhab.core.config.discovery.DiscoveryService;
import org.openhab.core.io.net.http.HttpClientFactory;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerFactory;
import org.osgi.framework.ServiceRegistration;
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 NanoleafHandlerFactory} is responsible for creating the controller (bridge)
* and panel (thing) handlers.
*
* @author Martin Raepple - Initial contribution
*/
@NonNullByDefault
@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()));
private final Logger logger = LoggerFactory.getLogger(NanoleafHandlerFactory.class);
private final Map<ThingUID, ServiceRegistration<?>> discoveryServiceRegs = new HashMap<>();
private final HttpClient httpClient;
@Activate
public NanoleafHandlerFactory(@Reference final HttpClientFactory httpClientFactory) {
this.httpClient = httpClientFactory.getCommonHttpClient();
}
@Override
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
}
@Override
protected @Nullable ThingHandler createHandler(Thing thing) {
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
if (THING_TYPE_CONTROLLER.equals(thingTypeUID)) {
NanoleafControllerHandler handler = new NanoleafControllerHandler((Bridge) thing, httpClient);
registerDiscoveryService(handler);
logger.debug("Nanoleaf controller handler created.");
return handler;
} else if (THING_TYPE_LIGHT_PANEL.equals(thingTypeUID)) {
NanoleafPanelHandler handler = new NanoleafPanelHandler(thing, httpClient);
logger.debug("Nanoleaf panel handler created.");
return handler;
}
return null;
}
@Override
protected void removeHandler(ThingHandler thingHandler) {
if (thingHandler instanceof NanoleafControllerHandler) {
unregisterDiscoveryService(thingHandler.getThing());
logger.debug("Nanoleaf controller handler removed.");
}
}
private synchronized void registerDiscoveryService(NanoleafControllerHandler bridgeHandler) {
NanoleafPanelsDiscoveryService discoveryService = new NanoleafPanelsDiscoveryService(bridgeHandler);
discoveryServiceRegs.put(bridgeHandler.getThing().getUID(),
bundleContext.registerService(DiscoveryService.class.getName(), discoveryService, new Hashtable<>()));
logger.debug("Discovery service for panels registered.");
}
@SuppressWarnings("null")
private synchronized void unregisterDiscoveryService(Thing thing) {
@Nullable
ServiceRegistration<?> serviceReg = discoveryServiceRegs.remove(thing.getUID());
// would require null check but "if (response!=null)" throws warning on comoile time :´-(
if (serviceReg != null) {
serviceReg.unregister();
}
logger.debug("Discovery service for panels removed.");
}
}

View File

@@ -0,0 +1,30 @@
/**
* Copyright (c) 2010-2020 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.nanoleaf.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Exception if request to Nanoleaf OpenAPI has been interrupted which is normally intended
*
* @author Stefan Höhn - Initial contribution
*/
@NonNullByDefault
public class NanoleafInterruptedException extends NanoleafException {
private static final long serialVersionUID = -6941678941424234257L;
public NanoleafInterruptedException(String message, InterruptedException interruptedException) {
super(message);
}
}

View File

@@ -0,0 +1,30 @@
/**
* Copyright (c) 2010-2020 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.nanoleaf.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Exception if request to Nanoleaf OpenAPI does not return any data
*
* @author Stefan Höhn - Initial contribution
*/
@NonNullByDefault
public class NanoleafNotFoundException extends NanoleafException {
private static final long serialVersionUID = -6941678941424573256L;
public NanoleafNotFoundException(String message) {
super(message);
}
}

View File

@@ -0,0 +1,30 @@
/**
* Copyright (c) 2010-2020 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.nanoleaf.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Exception if request to Nanoleaf OpenAPI is unauthorized (e.g. invalid or missing auth token)
*
* @author Martin Raepple - Initial contribution
*/
@NonNullByDefault
public class NanoleafUnauthorizedException extends NanoleafException {
private static final long serialVersionUID = -6941678941424573257L;
public NanoleafUnauthorizedException(String message) {
super(message);
}
}

View File

@@ -0,0 +1,169 @@
/**
* Copyright (c) 2010-2020 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.nanoleaf.internal;
import static org.openhab.binding.nanoleaf.internal.NanoleafBindingConstants.*;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.Iterator;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeoutException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
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.client.api.ContentResponse;
import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.HttpScheme;
import org.eclipse.jetty.http.HttpStatus;
import org.openhab.binding.nanoleaf.internal.config.NanoleafControllerConfig;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link OpenAPIUtils} offers helper methods to support API communication with the controller
*
* @author Martin Raepple - Initial contribution
*/
@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+)");
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);
}
public static URI getUri(NanoleafControllerConfig controllerConfig, String apiOperation, @Nullable String query)
throws NanoleafException {
String path;
// get network settings from configuration
String address = controllerConfig.address;
int port = controllerConfig.port;
if (API_ADD_USER.equals(apiOperation)) {
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 {
throw new NanoleafUnauthorizedException("No authentication token found in configuration");
}
}
URI requestURI;
try {
requestURI = new URI(HttpScheme.HTTP.asString(), null, address, port, path, query, null);
} catch (URISyntaxException use) {
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();
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 (openAPIResponse.getStatus() == HttpStatus.UNAUTHORIZED_401) {
throw new NanoleafUnauthorizedException("OpenAPI request unauthorized");
} else if (openAPIResponse.getStatus() == HttpStatus.NOT_FOUND_404) {
throw new NanoleafNotFoundException("OpenAPI request did not get any result back");
} else if (openAPIResponse.getStatus() == HttpStatus.BAD_REQUEST_400) {
throw new NanoleafBadRequestException(
String.format("Nanoleaf did not expect this request. HTTP response code %s",
openAPIResponse.getStatus()));
} else {
throw new NanoleafException(String.format("OpenAPI request failed. HTTP response code %s",
openAPIResponse.getStatus()));
}
}
} catch (ExecutionException | TimeoutException clientException) {
if (clientException.getCause() instanceof HttpResponseException
&& ((HttpResponseException) clientException.getCause()).getResponse()
.getStatus() == HttpStatus.UNAUTHORIZED_401) {
LOGGER.warn("OpenAPI request unauthorized. Invalid authorization token.");
throw new NanoleafUnauthorizedException("Invalid authorization token");
}
throw new NanoleafException("Failed to send OpenAPI request", clientException);
} catch (InterruptedException interruptedException) {
throw new NanoleafInterruptedException("OpenAPI request has been interrupted", interruptedException);
}
}
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) {
while (iter.hasNext()) {
@Nullable
ByteBuffer buffer = iter.next();
LOGGER.trace("Content {}", StandardCharsets.UTF_8.decode(buffer).toString());
}
}
}
}
public static boolean checkRequiredFirmware(String modelId, String currentFirmwareVersion) {
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;
}
private static int[] getFirmwareVersionNumbers(String firmwareVersion) throws IllegalArgumentException {
Matcher m = FIRMWARE_VERSION_PATTERN.matcher(firmwareVersion);
if (!m.matches()) {
throw new IllegalArgumentException("Malformed controller firmware version");
}
return new int[] { Integer.parseInt(m.group(1)), Integer.parseInt(m.group(2)), Integer.parseInt(m.group(3)) };
}
}

View File

@@ -0,0 +1,41 @@
/**
* Copyright (c) 2010-2020 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.nanoleaf.internal.config;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* The {@link NanoleafControllerConfig} class contains fields mapping controller configuration parameters.
*
* @author Martin Raepple - Initial contribution
*/
@NonNullByDefault
public class NanoleafControllerConfig {
/** IP address or hostname of the light panels controller */
public static final String ADDRESS = "address";
public String address = "";
/** Port number of the light panels controller */
public static final String PORT = "port";
public int port = 16021;
/** Authorization token for controller API */
public static final String AUTH_TOKEN = "authToken";
public @Nullable String authToken;
/** Light panels status refresh interval */
public static final String REFRESH_INTERVAL = "refreshInterval";
public int refreshInterval = 60;
/** Nanoleaf device type: Light panels or Canvas */
public static final String DEVICE_TYPE = "deviceType";
public String deviceType = "lightPanels";
}

View File

@@ -0,0 +1,36 @@
/**
* Copyright (c) 2010-2020 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.nanoleaf.internal.config;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link NanoleafPanelConfig} class contains fields mapping an individual panel configuration parameters.
*
* @author Martin Raepple - Initial contribution
*/
@NonNullByDefault
public class NanoleafPanelConfig {
/** ID of the light panel assigned by the controller */
public static final String ID = "id";
public Integer id = 0;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
}

View File

@@ -0,0 +1,114 @@
/**
* Copyright (c) 2010-2020 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.nanoleaf.internal.discovery;
import static org.openhab.binding.nanoleaf.internal.NanoleafBindingConstants.*;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import javax.jmdns.ServiceInfo;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.nanoleaf.internal.NanoleafHandlerFactory;
import org.openhab.binding.nanoleaf.internal.OpenAPIUtils;
import org.openhab.core.config.discovery.DiscoveryResult;
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
import org.openhab.core.config.discovery.mdns.MDNSDiscoveryParticipant;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.ThingUID;
import org.osgi.service.component.annotations.Component;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link NanoleafMDNSDiscoveryParticipant} is responsible for discovering new Nanoleaf controllers (bridges).
*
* @author Martin Raepple - Initial contribution
* @author Stefan Höhn - further improvements for static defined things
* @see <a href="https://openhab.org/documentation/development/bindings/discovery-services.html">MSDN
* Discovery</a>
*/
@Component(immediate = true, configurationPid = "discovery.nanoleaf")
@NonNullByDefault
public class NanoleafMDNSDiscoveryParticipant implements MDNSDiscoveryParticipant {
private final Logger logger = LoggerFactory.getLogger(NanoleafMDNSDiscoveryParticipant.class);
@Override
public Set<ThingTypeUID> getSupportedThingTypeUIDs() {
return NanoleafHandlerFactory.SUPPORTED_THING_TYPES_UIDS;
}
@Override
public String getServiceType() {
return SERVICE_TYPE;
}
@Override
public @Nullable DiscoveryResult createResult(ServiceInfo service) {
final ThingUID uid = getThingUID(service);
if (uid == null) {
return null;
}
final Map<String, Object> properties = new HashMap<>(2);
String host = service.getHostAddresses()[0];
properties.put(CONFIG_ADDRESS, host);
int port = service.getPort();
properties.put(CONFIG_PORT, port);
String firmwareVersion = service.getPropertyString("srcvers");
properties.put(Thing.PROPERTY_FIRMWARE_VERSION, firmwareVersion);
String modelId = service.getPropertyString("md");
properties.put(Thing.PROPERTY_MODEL_ID, modelId);
properties.put(Thing.PROPERTY_VENDOR, "Nanoleaf");
String qualifiedName = service.getQualifiedName();
logger.debug("AVR found: {}", qualifiedName);
logger.trace("Discovered nanoleaf host: {} port: {} firmWare: {} modelId: {} qualifiedName: {}", host, port,
firmwareVersion, modelId, qualifiedName);
logger.debug("Adding Nanoleaf controller {} with FW version {} found at {} {} to inbox", qualifiedName,
firmwareVersion, host, port);
if (!OpenAPIUtils.checkRequiredFirmware(service.getPropertyString("md"), firmwareVersion)) {
logger.warn("Nanoleaf controller firmware is too old. Must be {} or higher",
MODEL_ID_LIGHTPANELS.equals(modelId) ? API_MIN_FW_VER_LIGHTPANELS : API_MIN_FW_VER_CANVAS);
}
final DiscoveryResult result = DiscoveryResultBuilder.create(uid).withThingType(getThingType(service))
.withProperties(properties).withLabel(service.getName()).withRepresentationProperty(CONFIG_ADDRESS)
.build();
return result;
}
@Override
public @Nullable ThingUID getThingUID(ServiceInfo service) {
ThingTypeUID thingTypeUID = getThingType(service);
if (thingTypeUID != null) {
String id = service.getPropertyString("id").replace(":", "");
return new ThingUID(thingTypeUID, id);
} else {
return null;
}
}
private @Nullable ThingTypeUID getThingType(final ServiceInfo service) {
String model = service.getPropertyString("md"); // model
logger.debug("Nanoleaf Type: {}", model);
if (model == null) {
return null;
}
return THING_TYPE_CONTROLLER;
}
}

View File

@@ -0,0 +1,118 @@
/**
* Copyright (c) 2010-2020 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.nanoleaf.internal.discovery;
import static org.openhab.binding.nanoleaf.internal.NanoleafBindingConstants.CONFIG_PANEL_ID;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.nanoleaf.internal.NanoleafBindingConstants;
import org.openhab.binding.nanoleaf.internal.NanoleafControllerListener;
import org.openhab.binding.nanoleaf.internal.NanoleafHandlerFactory;
import org.openhab.binding.nanoleaf.internal.handler.NanoleafControllerHandler;
import org.openhab.binding.nanoleaf.internal.model.ControllerInfo;
import org.openhab.binding.nanoleaf.internal.model.Layout;
import org.openhab.binding.nanoleaf.internal.model.PanelLayout;
import org.openhab.binding.nanoleaf.internal.model.PositionDatum;
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.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link NanoleafPanelsDiscoveryService} is responsible for discovering the individual
* panels connected to the controller.
*
* @author Martin Raepple - Initial contribution
*/
@NonNullByDefault
public class NanoleafPanelsDiscoveryService extends AbstractDiscoveryService implements NanoleafControllerListener {
private static final int SEARCH_TIMEOUT_SECONDS = 60;
private final Logger logger = LoggerFactory.getLogger(NanoleafPanelsDiscoveryService.class);
private final NanoleafControllerHandler bridgeHandler;
/**
* Constructs a new {@link NanoleafPanelsDiscoveryService} attached to the given bridge handler.
*
* @param nanoleafControllerHandler The bridge handler this discovery service is attached to
*/
public NanoleafPanelsDiscoveryService(NanoleafControllerHandler nanoleafControllerHandler) {
super(NanoleafHandlerFactory.SUPPORTED_THING_TYPES_UIDS, SEARCH_TIMEOUT_SECONDS, false);
this.bridgeHandler = nanoleafControllerHandler;
}
@Override
protected void startScan() {
logger.debug("Starting Nanoleaf panel discovery");
bridgeHandler.registerControllerListener(this);
}
@Override
protected synchronized void stopScan() {
logger.debug("Stopping Nanoleaf panel discovery");
super.stopScan();
bridgeHandler.unregisterControllerListener(this);
}
/**
* Called by the controller handler with bridge and panel data
*
* @param bridge The controller
* @param controllerInfo Panel data (and more)
*/
@Override
public void onControllerInfoFetched(ThingUID bridge, ControllerInfo controllerInfo) {
logger.debug("Discover panels connected to controller with id {}", bridge.getAsString());
final PanelLayout panelLayout = controllerInfo.getPanelLayout();
@Nullable
Layout layout = panelLayout.getLayout();
if (layout != null && layout.getNumPanels() > 0) {
@Nullable
final List<PositionDatum> positionData = layout.getPositionData();
if (positionData != null) {
Iterator<PositionDatum> iterator = positionData.iterator();
while (iterator.hasNext()) {
@Nullable
PositionDatum panel = iterator.next();
ThingUID newPanelThingUID = new ThingUID(NanoleafBindingConstants.THING_TYPE_LIGHT_PANEL, bridge,
Integer.toString(panel.getPanelId()));
final Map<String, Object> properties = new HashMap<>(1);
properties.put(CONFIG_PANEL_ID, panel.getPanelId());
DiscoveryResult newPanel = DiscoveryResultBuilder.create(newPanelThingUID).withBridge(bridge)
.withProperties(properties).withLabel("Light Panel " + panel.getPanelId())
.withRepresentationProperty(CONFIG_PANEL_ID).build();
logger.debug("Adding panel with id {} to inbox", panel.getPanelId());
thingDiscovered(newPanel);
}
} else {
logger.debug("Couldn't add panels to inbox as layout position data was null");
}
} else {
logger.info("No panels found or connected to controller");
}
}
}

View File

@@ -0,0 +1,855 @@
/**
* Copyright (c) 2010-2020 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.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.List;
import java.util.Map;
import java.util.Scanner;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import org.apache.commons.lang.StringUtils;
import org.eclipse.jdt.annotation.NonNullByDefault;
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.*;
import org.openhab.binding.nanoleaf.internal.config.NanoleafControllerConfig;
import org.openhab.binding.nanoleaf.internal.model.*;
import org.openhab.core.config.core.Configuration;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.HSBType;
import org.openhab.core.library.types.IncreaseDecreaseType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.PercentType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.thing.Bridge;
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.binding.BaseBridgeHandler;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
import com.google.gson.JsonSyntaxException;
/**
* The {@link NanoleafControllerHandler} is responsible for handling commands to the controller which
* affect all panels connected to it (e.g. selected effect)
*
* @author Martin Raepple - Initial contribution
* @author Stefan Höhn - Canvas Touch Support
*/
@NonNullByDefault
public class NanoleafControllerHandler extends BaseBridgeHandler {
// Pairing interval in seconds
private static final int PAIRING_INTERVAL = 25;
// Panel discovery interval in seconds
private static final int PANEL_DISCOVERY_INTERVAL = 30;
private final Logger logger = LoggerFactory.getLogger(NanoleafControllerHandler.class);
private HttpClient httpClient;
private List<NanoleafControllerListener> controllerListeners = new CopyOnWriteArrayList<>();
// Pairing, update and panel discovery jobs and touch event job
private @NonNullByDefault({}) ScheduledFuture<?> pairingJob;
private @NonNullByDefault({}) ScheduledFuture<?> updateJob;
private @NonNullByDefault({}) ScheduledFuture<?> panelDiscoveryJob;
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;
private @Nullable String authToken;
private @Nullable String deviceType;
private @NonNullByDefault({}) ControllerInfo controllerInfo;
public NanoleafControllerHandler(Bridge bridge, HttpClient httpClient) {
super(bridge);
this.httpClient = httpClient;
}
@Override
public void initialize() {
logger.debug("Initializing the controller (bridge)");
updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.BRIDGE_UNINITIALIZED);
NanoleafControllerConfig config = getConfigAs(NanoleafControllerConfig.class);
setAddress(config.address);
setPort(config.port);
setRefreshIntervall(config.refreshInterval);
setAuthToken(config.authToken);
@Nullable
String property = getThing().getProperties().get(Thing.PROPERTY_MODEL_ID);
if (MODEL_ID_CANVAS.equals(property)) {
config.deviceType = DEVICE_TYPE_CANVAS;
} else {
config.deviceType = DEVICE_TYPE_LIGHTPANELS;
}
setDeviceType(config.deviceType);
try {
if (StringUtils.isEmpty(getAddress()) || StringUtils.isEmpty(String.valueOf(getPort()))) {
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 (!StringUtils.isEmpty(getThing().getProperties().get(Thing.PROPERTY_FIRMWARE_VERSION))
&& !OpenAPIUtils.checkRequiredFirmware(getThing().getProperties().get(Thing.PROPERTY_MODEL_ID),
getThing().getProperties().get(Thing.PROPERTY_FIRMWARE_VERSION))) {
logger.warn("Nanoleaf controller firmware is too old: {}. Must be equal or higher than {}",
getThing().getProperties().get(Thing.PROPERTY_FIRMWARE_VERSION), API_MIN_FW_VER_LIGHTPANELS);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"@text/error.nanoleaf.controller.incompatibleFirmware");
stopAllJobs();
} else if (StringUtils.isEmpty(getAuthToken())) {
logger.debug("No token found. Start pairing background job");
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING,
"@text/error.nanoleaf.controller.noToken");
startPairingJob();
stopUpdateJob();
stopPanelDiscoveryJob();
} else {
logger.debug("Controller is online. Stop pairing job, start update & panel discovery jobs");
updateStatus(ThingStatus.ONLINE);
stopPairingJob();
startUpdateJob();
startPanelDiscoveryJob();
startTouchJob();
}
} catch (IllegalArgumentException iae) {
logger.warn("Nanoleaf controller firmware version not in format x.y.z: {}",
getThing().getProperties().get(Thing.PROPERTY_FIRMWARE_VERSION));
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"@text/error.nanoleaf.controller.incompatibleFirmware");
}
}
@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_POWER:
case CHANNEL_COLOR:
case CHANNEL_COLOR_TEMPERATURE:
case CHANNEL_COLOR_TEMPERATURE_ABS:
case CHANNEL_PANEL_LAYOUT:
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 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() {
// delete token for openHAB
ContentResponse deleteTokenResponse;
try {
Request deleteTokenRequest = OpenAPIUtils.requestBuilder(httpClient, getControllerConfig(), API_DELETE_USER,
HttpMethod.DELETE);
deleteTokenResponse = OpenAPIUtils.sendOpenAPIRequest(deleteTokenRequest);
if (deleteTokenResponse.getStatus() != HttpStatus.NO_CONTENT_204) {
logger.warn("Failed to delete token for openHAB. Response code is {}", deleteTokenResponse.getStatus());
return;
}
logger.debug("Successfully deleted token for openHAB from controller");
} catch (NanoleafUnauthorizedException e) {
logger.warn("Attempt to delete token for openHAB failed. Token unauthorized.");
} catch (NanoleafException ne) {
logger.warn("Attempt to delete token for openHAB failed : {}", ne.getMessage());
}
stopAllJobs();
super.handleRemoval();
logger.debug("Nanoleaf controller removed");
}
@Override
public void dispose() {
stopAllJobs();
super.dispose();
logger.debug("Disposing handler for Nanoleaf controller {}", getThing().getUID());
}
public boolean registerControllerListener(NanoleafControllerListener controllerListener) {
logger.debug("Register new listener for controller {}", getThing().getUID());
boolean result = controllerListeners.add(controllerListener);
if (result) {
startPanelDiscoveryJob();
}
return result;
}
public boolean unregisterControllerListener(NanoleafControllerListener controllerListener) {
logger.debug("Unregister listener for controller {}", getThing().getUID());
boolean result = controllerListeners.remove(controllerListener);
if (result) {
stopPanelDiscoveryJob();
}
return result;
}
public NanoleafControllerConfig getControllerConfig() {
NanoleafControllerConfig config = new NanoleafControllerConfig();
config.address = this.getAddress();
config.port = this.getPort();
config.refreshInterval = this.getRefreshIntervall();
config.authToken = this.getAuthToken();
config.deviceType = this.getDeviceType();
return config;
}
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);
}
}
private synchronized void stopPairingJob() {
if (pairingJob != null && !pairingJob.isCancelled()) {
logger.debug("Stop pairing job");
pairingJob.cancel(true);
this.pairingJob = null;
}
}
private synchronized void startUpdateJob() {
if (StringUtils.isNotEmpty(getAuthToken())) {
if (updateJob == null || updateJob.isCancelled()) {
logger.debug("Start controller status job, repeat every {} sec", getRefreshIntervall());
updateJob = scheduler.scheduleWithFixedDelay(this::runUpdate, 0, getRefreshIntervall(),
TimeUnit.SECONDS);
}
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING,
"@text/error.nanoleaf.controller.noToken");
}
}
private synchronized void stopUpdateJob() {
if (updateJob != null && !updateJob.isCancelled()) {
logger.debug("Stop status job");
updateJob.cancel(true);
this.updateJob = null;
}
}
public synchronized void startPanelDiscoveryJob() {
logger.debug("Starting panel discovery job. Has Controller-Listeners: {} panelDiscoveryJob: {}",
!controllerListeners.isEmpty(), panelDiscoveryJob);
if (!controllerListeners.isEmpty() && (panelDiscoveryJob == null || panelDiscoveryJob.isCancelled())) {
logger.debug("Start panel discovery job, interval={} sec", PANEL_DISCOVERY_INTERVAL);
panelDiscoveryJob = scheduler.scheduleWithFixedDelay(this::runPanelDiscovery, 0, PANEL_DISCOVERY_INTERVAL,
TimeUnit.SECONDS);
}
}
private synchronized void stopPanelDiscoveryJob() {
if (controllerListeners.isEmpty() && panelDiscoveryJob != null && !panelDiscoveryJob.isCancelled()) {
logger.debug("Stop panel discovery job");
panelDiscoveryJob.cancel(true);
this.panelDiscoveryJob = null;
}
}
private synchronized void startTouchJob() {
NanoleafControllerConfig config = getConfigAs(NanoleafControllerConfig.class);
if (!config.deviceType.equals(DEVICE_TYPE_CANVAS)) {
logger.debug("NOT starting TouchJob for Panel {} because it has wrong device type '{}' vs required '{}'",
this.getThing().getUID(), config.deviceType, DEVICE_TYPE_CANVAS);
return;
} else
logger.debug("Starting TouchJob for Panel {}", this.getThing().getUID());
if (StringUtils.isNotEmpty(getAuthToken())) {
if (touchJob == null || touchJob.isCancelled()) {
logger.debug("Starting Touchjob now");
touchJob = scheduler.schedule(this::runTouchDetection, 0, TimeUnit.SECONDS);
}
} else {
logger.error("starting TouchJob for Controller {} failed - missing token", this.getThing().getUID());
}
}
private synchronized void stopTouchJob() {
if (touchJob != null && !touchJob.isCancelled()) {
logger.debug("Stop touch job");
touchJob.cancel(true);
this.touchJob = null;
}
}
private void runUpdate() {
logger.debug("Run update job");
try {
updateFromControllerInfo();
startTouchJob(); // if device type has changed, start touch detection.
// controller might have been offline, e.g. for firmware update. In this case, return to online state
if (ThingStatus.OFFLINE.equals(getThing().getStatus())) {
logger.debug("Controller {} is back online", thing.getUID());
updateStatus(ThingStatus.ONLINE);
}
} catch (NanoleafUnauthorizedException nae) {
logger.warn("Status update unauthorized: {}", nae.getMessage());
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"@text/error.nanoleaf.controller.invalidToken");
if (StringUtils.isEmpty(getAuthToken())) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING,
"@text/error.nanoleaf.controller.noToken");
}
} catch (NanoleafException ne) {
logger.warn("Status update failed: {}", ne.getMessage());
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"@text/error.nanoleaf.controller.communication");
} catch (RuntimeException e) {
logger.warn("Update job failed", e);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "@text/error.nanoleaf.controller.runtime");
}
}
private void runPairing() {
logger.debug("Run pairing job");
try {
if (StringUtils.isNotEmpty(getAuthToken())) {
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).send();
if (logger.isTraceEnabled()) {
logger.trace("Auth token response: {}", authTokenResponse.getContentAsString());
}
if (authTokenResponse.getStatus() != HttpStatus.OK_200) {
logger.debug("Pairing pending for {}. Controller returns status code {}", this.getThing().getUID(),
authTokenResponse.getStatus());
} else {
// get auth token from response
@Nullable
AuthToken authToken = gson.fromJson(authTokenResponse.getContentAsString(), AuthToken.class);
if (StringUtils.isNotEmpty(authToken.getAuthToken())) {
logger.debug("Pairing succeeded.");
// Update and save the auth token in the thing configuration
Configuration config = editConfiguration();
config.put(NanoleafControllerConfig.AUTH_TOKEN, authToken.getAuthToken());
updateConfiguration(config);
updateStatus(ThingStatus.ONLINE);
// Update local field
setAuthToken(authToken.getAuthToken());
stopPairingJob();
startUpdateJob();
startPanelDiscoveryJob();
startTouchJob();
} else {
logger.debug("No auth token found in response: {}", authTokenResponse.getContentAsString());
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"@text/error.nanoleaf.controller.pairingFailed");
throw new NanoleafException(authTokenResponse.getContentAsString());
}
}
} catch (JsonSyntaxException e) {
logger.warn("Received invalid data", e);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"@text/error.nanoleaf.controller.invalidData");
} catch (NanoleafException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"@text/error.nanoleaf.controller.noTokenReceived");
} catch (InterruptedException | ExecutionException | TimeoutException e) {
logger.warn("Cannot send authorization request to controller: ", e);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"@text/error.nanoleaf.controller.authRequest");
} catch (RuntimeException e) {
logger.warn("Pairing job failed", e);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "@text/error.nanoleaf.controller.runtime");
} catch (Exception e) {
logger.warn("Cannot start http client", e);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"@text/error.nanoleaf.controller.noClient");
}
}
private void runPanelDiscovery() {
logger.debug("Run panel discovery job");
// Trigger a new discovery of connected panels
for (NanoleafControllerListener controllerListener : controllerListeners) {
try {
controllerListener.onControllerInfoFetched(getThing().getUID(), receiveControllerInfo());
} catch (NanoleafUnauthorizedException nue) {
logger.warn("Panel discovery unauthorized: {}", nue.getMessage());
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"@text/error.nanoleaf.controller.invalidToken");
if (StringUtils.isEmpty(getAuthToken())) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING,
"@text/error.nanoleaf.controller.noToken");
}
} catch (NanoleafInterruptedException nie) {
logger.info("Panel discovery has been stopped.");
} catch (NanoleafException ne) {
logger.warn("Failed to discover panels: ", ne);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"@text/error.nanoleaf.controller.communication");
} catch (RuntimeException e) {
logger.warn("Panel discovery job failed", e);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "@text/error.nanoleaf.controller.runtime");
}
}
}
/**
* 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;
}
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);
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 {
@Nullable
TouchEvents touchEvents = gson.fromJson(json, TouchEvents.class);
handleTouchEvents(touchEvents);
} catch (JsonSyntaxException jse) {
logger.error("couldn't parse touch event json {}", json);
}
}
}
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
*
* @param touchEvents
*/
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 updateFromControllerInfo() throws NanoleafException {
logger.debug("Update channels for controller {}", thing.getUID());
this.controllerInfo = receiveControllerInfo();
if (controllerInfo == null) {
logger.debug("No Controller Info has been provided");
return;
}
final State state = controllerInfo.getState();
OnOffType powerState = state.getOnOff();
updateState(CHANNEL_POWER, powerState);
@Nullable
Ct colorTemperature = state.getColorTemperature();
float colorTempPercent = 0f;
if (colorTemperature != null) {
updateState(CHANNEL_COLOR_TEMPERATURE_ABS, new DecimalType(colorTemperature.getValue()));
@Nullable
Integer min = colorTemperature.getMin();
int colorMin = (min == null) ? 0 : min;
@Nullable
Integer max = colorTemperature.getMax();
int colorMax = (max == null) ? 0 : max;
colorTempPercent = (colorTemperature.getValue() - colorMin) / (colorMax - colorMin)
* 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
Sat stateSaturation = state.getSaturation();
int saturation = (stateSaturation != null) ? stateSaturation.getValue() : 0;
@Nullable
Brightness stateBrightness = state.getBrightness();
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)));
updateState(CHANNEL_COLOR_MODE, new StringType(state.getColorMode()));
updateState(CHANNEL_RHYTHM_ACTIVE, controllerInfo.getRhythm().getRhythmActive() ? OnOffType.ON : OnOffType.OFF);
updateState(CHANNEL_RHYTHM_MODE, new DecimalType(controllerInfo.getRhythm().getRhythmMode()));
updateState(CHANNEL_RHYTHM_STATE,
controllerInfo.getRhythm().getRhythmConnected() ? OnOffType.ON : OnOffType.OFF);
// update bridge properties which may have changed, or are not present during discovery
Map<String, String> properties = editProperties();
properties.put(Thing.PROPERTY_SERIAL_NUMBER, controllerInfo.getSerialNo());
properties.put(Thing.PROPERTY_FIRMWARE_VERSION, controllerInfo.getFirmwareVersion());
properties.put(Thing.PROPERTY_MODEL_ID, controllerInfo.getModel());
properties.put(Thing.PROPERTY_VENDOR, controllerInfo.getManufacturer());
updateProperties(properties);
Configuration config = editConfiguration();
if (MODEL_ID_CANVAS.equals(controllerInfo.getModel())) {
config.put(NanoleafControllerConfig.DEVICE_TYPE, DEVICE_TYPE_CANVAS);
logger.debug("Set to device type {}", DEVICE_TYPE_CANVAS);
} else {
config.put(NanoleafControllerConfig.DEVICE_TYPE, DEVICE_TYPE_LIGHTPANELS);
logger.debug("Set to device type {}", DEVICE_TYPE_LIGHTPANELS);
}
updateConfiguration(config);
getConfig().getProperties().forEach((key, value) -> {
logger.trace("Configuration property: key {} value {}", key, value);
});
getThing().getProperties().forEach((key, value) -> {
logger.debug("Thing property: key {} value {}", key, value);
});
// update the color channels of each panel
this.getThing().getThings().forEach(child -> {
NanoleafPanelHandler panelHandler = (NanoleafPanelHandler) child.getHandler();
if (panelHandler != null) {
logger.debug("Update color channel for panel {}", panelHandler.getThing().getUID());
panelHandler.updatePanelColorChannel();
}
});
}
private ControllerInfo receiveControllerInfo() throws NanoleafException, NanoleafUnauthorizedException {
ContentResponse controllerlInfoJSON = OpenAPIUtils.sendOpenAPIRequest(OpenAPIUtils.requestBuilder(httpClient,
getControllerConfig(), API_GET_CONTROLLER_INFO, HttpMethod.GET));
@Nullable
ControllerInfo controllerInfo = gson.fromJson(controllerlInfoJSON.getContentAsString(), ControllerInfo.class);
return controllerInfo;
}
private void sendStateCommand(String channel, Command command) throws NanoleafException {
State stateObject = new State();
switch (channel) {
case CHANNEL_POWER:
if (command instanceof OnOffType) {
// On/Off command - turns controller on/off
BooleanState state = new On();
state.setValue(OnOffType.ON.equals(command));
stateObject.setState(state);
} else {
logger.warn("Unhandled command type: {}", command.getClass().getName());
return;
}
break;
case CHANNEL_COLOR:
if (command instanceof OnOffType) {
// On/Off command - turns controller on/off
BooleanState state = new On();
state.setValue(OnOffType.ON.equals(command));
stateObject.setState(state);
} else if (command instanceof HSBType) {
// regular color HSB command
IntegerState h = new Hue();
IntegerState s = new Sat();
IntegerState b = new Brightness();
h.setValue(((HSBType) command).getHue().intValue());
s.setValue(((HSBType) command).getSaturation().intValue());
b.setValue(((HSBType) command).getBrightness().intValue());
stateObject.setState(h);
stateObject.setState(s);
stateObject.setState(b);
} else if (command instanceof PercentType) {
// brightness command
IntegerState b = new Brightness();
b.setValue(((PercentType) command).intValue());
stateObject.setState(b);
} else if (command instanceof IncreaseDecreaseType) {
// increase/decrease brightness
if (controllerInfo != null) {
@Nullable
Brightness brightness = controllerInfo.getState().getBrightness();
int brightnessMin = 0;
int brightnessMax = 0;
if (brightness != null) {
@Nullable
Integer min = brightness.getMin();
brightnessMin = (min == null) ? 0 : min;
@Nullable
Integer max = brightness.getMax();
brightnessMax = (max == null) ? 0 : max;
if (IncreaseDecreaseType.INCREASE.equals(command)) {
brightness.setValue(
Math.min(brightnessMax, brightness.getValue() + BRIGHTNESS_STEP_SIZE));
} else {
brightness.setValue(
Math.max(brightnessMin, brightness.getValue() - BRIGHTNESS_STEP_SIZE));
}
stateObject.setState(brightness);
logger.debug("Setting controller brightness to {}", brightness.getValue());
// update controller info in case new command is sent before next update job interval
controllerInfo.getState().setBrightness(brightness);
} else {
logger.debug("Couldn't set brightness as it was null!");
}
}
} else {
logger.warn("Unhandled command type: {}", command.getClass().getName());
return;
}
break;
case CHANNEL_COLOR_TEMPERATURE:
if (command instanceof PercentType) {
// Color temperature (percent)
IntegerState state = new Ct();
@Nullable
Ct colorTemperature = controllerInfo.getState().getColorTemperature();
int colorMin = 0;
int colorMax = 0;
if (colorTemperature != null) {
@Nullable
Integer min = colorTemperature.getMin();
colorMin = (min == null) ? 0 : min;
@Nullable
Integer max = colorTemperature.getMax();
colorMax = (max == null) ? 0 : max;
}
state.setValue(Math.round((colorMax - colorMin) * ((PercentType) command).intValue()
/ PercentType.HUNDRED.floatValue() + colorMin));
stateObject.setState(state);
} else {
logger.warn("Unhandled command type: {}", command.getClass().getName());
return;
}
break;
case CHANNEL_COLOR_TEMPERATURE_ABS:
if (command instanceof DecimalType) {
// Color temperature (absolute)
IntegerState state = new Ct();
state.setValue(((DecimalType) command).intValue());
stateObject.setState(state);
} else {
logger.warn("Unhandled command type: {}", command.getClass().getName());
return;
}
break;
case CHANNEL_PANEL_LAYOUT:
@Nullable
Layout layout = controllerInfo.getPanelLayout().getLayout();
String layoutView = (layout != null) ? layout.getLayoutView() : "";
logger.info("Panel layout and ids for controller {} \n{}", thing.getUID(), layoutView);
updateState(CHANNEL_PANEL_LAYOUT, OnOffType.OFF);
break;
default:
logger.warn("Unhandled command type: {}", command.getClass().getName());
return;
}
Request setNewStateRequest = OpenAPIUtils.requestBuilder(httpClient, getControllerConfig(), API_SET_VALUE,
HttpMethod.PUT);
setNewStateRequest.content(new StringContentProvider(gson.toJson(stateObject)), "application/json");
OpenAPIUtils.sendOpenAPIRequest(setNewStateRequest);
}
private void sendEffectCommand(Command command) throws NanoleafException {
Effects effects = new Effects();
if (command instanceof StringType) {
effects.setSelect(command.toString());
} 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());
} 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 String getAddress() {
return StringUtils.defaultString(this.address);
}
private void setAddress(String address) {
this.address = address;
}
private int getPort() {
return port;
}
private void setPort(int port) {
this.port = port;
}
private int getRefreshIntervall() {
return refreshIntervall;
}
private void setRefreshIntervall(int refreshIntervall) {
this.refreshIntervall = refreshIntervall;
}
private String getAuthToken() {
return StringUtils.defaultString(authToken);
}
private void setAuthToken(@Nullable String authToken) {
this.authToken = authToken;
}
private String getDeviceType() {
return StringUtils.defaultString(deviceType);
}
private void setDeviceType(String deviceType) {
this.deviceType = deviceType;
}
private void stopAllJobs() {
stopPairingJob();
stopUpdateJob();
stopPanelDiscoveryJob();
stopTouchJob();
}
}

View File

@@ -0,0 +1,379 @@
/**
* Copyright (c) 2010-2020 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.nanoleaf.internal.handler;
import static org.openhab.binding.nanoleaf.internal.NanoleafBindingConstants.*;
import java.math.BigDecimal;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ScheduledFuture;
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.api.ContentResponse;
import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.client.util.StringContentProvider;
import org.eclipse.jetty.http.HttpMethod;
import org.openhab.binding.nanoleaf.internal.*;
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.library.types.*;
import org.openhab.core.thing.Bridge;
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.thing.binding.BridgeHandler;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
/**
* The {@link NanoleafPanelHandler} is responsible for handling commands to the controller which
* affect an individual panels
*
* @author Martin Raepple - Initial contribution
* @author Stefan Höhn - Canvas Touch Support
*/
@NonNullByDefault
public class NanoleafPanelHandler extends BaseThingHandler {
private static final PercentType MIN_PANEL_BRIGHTNESS = PercentType.ZERO;
private static final PercentType MAX_PANEL_BRIGHTNESS = PercentType.HUNDRED;
private final Logger logger = LoggerFactory.getLogger(NanoleafPanelHandler.class);
private HttpClient httpClient;
// JSON parser for API responses
private final Gson gson = new Gson();
// holds current color data per panel
private Map<String, HSBType> panelInfo = new HashMap<>();
private @NonNullByDefault({}) ScheduledFuture<?> singleTapJob;
private @NonNullByDefault({}) ScheduledFuture<?> doubleTapJob;
public NanoleafPanelHandler(Thing thing, HttpClient httpClient) {
super(thing);
this.httpClient = httpClient;
}
@Override
public void initialize() {
logger.debug("Initializing handler for panel {}", getThing().getUID());
updateStatus(ThingStatus.OFFLINE);
Bridge controller = getBridge();
if (controller == null) {
initializePanel(new ThingStatusInfo(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED, ""));
} else if (ThingStatus.OFFLINE.equals(controller.getStatus())) {
initializePanel(new ThingStatusInfo(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE,
"@text/error.nanoleaf.panel.controllerOffline"));
} else {
initializePanel(controller.getStatusInfo());
}
}
@Override
public void bridgeStatusChanged(ThingStatusInfo controllerStatusInfo) {
logger.debug("Controller status changed to {} -- {}", controllerStatusInfo,
controllerStatusInfo.getDescription() + "/" + controllerStatusInfo.getStatus() + "/"
+ controllerStatusInfo.hashCode());
if (controllerStatusInfo.getStatus().equals(ThingStatus.OFFLINE)) {
initializePanel(new ThingStatusInfo(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE,
"@text/error.nanoleaf.panel.controllerOffline"));
} else {
initializePanel(controllerStatusInfo);
}
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
logger.debug("Received command {} for channel {}", command, channelUID);
try {
switch (channelUID.getId()) {
case CHANNEL_PANEL_COLOR:
sendRenderedEffectCommand(command);
break;
default:
logger.warn("Channel with id {} not handled", channelUID.getId());
break;
}
} catch (NanoleafUnauthorizedException nae) {
logger.warn("Authorization for command {} for channelUID {} failed: {}", command, channelUID,
nae.getMessage());
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"@text/error.nanoleaf.controller.invalidToken");
} catch (NanoleafException ne) {
logger.warn("Handling command {} for channelUID {} failed: {}", command, channelUID, ne.getMessage());
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"@text/error.nanoleaf.controller.communication");
}
}
@Override
public void handleRemoval() {
logger.debug("Nanoleaf panel {} removed", getThing().getUID());
super.handleRemoval();
}
@Override
public void dispose() {
logger.debug("Disposing handler for Nanoleaf panel {}", getThing().getUID());
stopAllJobs();
super.dispose();
}
private void stopAllJobs() {
if (singleTapJob != null && !singleTapJob.isCancelled()) {
logger.debug("Stop single touch job");
singleTapJob.cancel(true);
this.singleTapJob = null;
}
if (doubleTapJob != null && !doubleTapJob.isCancelled()) {
logger.debug("Stop double touch job");
doubleTapJob.cancel(true);
this.doubleTapJob = null;
}
}
private void initializePanel(ThingStatusInfo panelStatus) {
updateStatus(panelStatus.getStatus(), panelStatus.getStatusDetail());
logger.debug("Panel {} status changed to {}-{}", this.getThing().getUID(), panelStatus.getStatus(),
panelStatus.getStatusDetail());
}
private void sendRenderedEffectCommand(Command command) throws NanoleafException {
logger.debug("Command Type: {}", command.getClass());
HSBType currentPanelColor = getPanelColor();
if (currentPanelColor != null)
logger.debug("currentPanelColor: {}", currentPanelColor.toString());
HSBType newPanelColor = new HSBType();
if (command instanceof HSBType) {
newPanelColor = (HSBType) command;
} else if (command instanceof OnOffType && (currentPanelColor != null)) {
if (OnOffType.ON.equals(command)) {
newPanelColor = new HSBType(currentPanelColor.getHue(), currentPanelColor.getSaturation(),
MAX_PANEL_BRIGHTNESS);
} else {
newPanelColor = new HSBType(currentPanelColor.getHue(), currentPanelColor.getSaturation(),
MIN_PANEL_BRIGHTNESS);
}
} else if (command instanceof PercentType && (currentPanelColor != null)) {
PercentType brightness = new PercentType(
Math.max(MIN_PANEL_BRIGHTNESS.intValue(), ((PercentType) command).intValue()));
newPanelColor = new HSBType(currentPanelColor.getHue(), currentPanelColor.getSaturation(), brightness);
} else if (command instanceof IncreaseDecreaseType && (currentPanelColor != null)) {
int brightness = currentPanelColor.getBrightness().intValue();
if (command.equals(IncreaseDecreaseType.INCREASE)) {
brightness = Math.min(MAX_PANEL_BRIGHTNESS.intValue(), brightness + BRIGHTNESS_STEP_SIZE);
} else {
brightness = Math.max(MIN_PANEL_BRIGHTNESS.intValue(), brightness - BRIGHTNESS_STEP_SIZE);
}
newPanelColor = new HSBType(currentPanelColor.getHue(), currentPanelColor.getSaturation(),
new PercentType(brightness));
} else if (command instanceof RefreshType) {
logger.debug("Refresh command received");
return;
} else {
logger.warn("Unhandled command type: {}", command.getClass().getName());
return;
}
// store panel's new HSB value
logger.trace("Setting new color {}", newPanelColor);
panelInfo.put(getThing().getConfiguration().get(CONFIG_PANEL_ID).toString(), newPanelColor);
// transform to RGB
PercentType[] rgbPercent = newPanelColor.toRGB();
logger.trace("Setting new rgbpercent {} {} {}", rgbPercent[0], rgbPercent[1], rgbPercent[2]);
int red = rgbPercent[0].toBigDecimal().divide(BigDecimal.valueOf(100), 2, BigDecimal.ROUND_HALF_UP)
.multiply(new BigDecimal(255)).intValue();
int green = rgbPercent[1].toBigDecimal().divide(BigDecimal.valueOf(100), 2, BigDecimal.ROUND_HALF_UP)
.multiply(new BigDecimal(255)).intValue();
int blue = rgbPercent[2].toBigDecimal().divide(BigDecimal.valueOf(100), 2, BigDecimal.ROUND_HALF_UP)
.multiply(new BigDecimal(255)).intValue();
logger.trace("Setting new rgb {} {} {}", red, green, blue);
Bridge bridge = getBridge();
if (bridge != null) {
Effects effects = new Effects();
Write write = new Write();
write.setCommand("display");
write.setAnimType("static");
String panelID = this.thing.getConfiguration().get(CONFIG_PANEL_ID).toString();
@Nullable
BridgeHandler handler = bridge.getHandler();
if (handler != null) {
NanoleafControllerConfig config = ((NanoleafControllerHandler) handler).getControllerConfig();
// Light Panels and Canvas use different stream commands
if (config.deviceType.equals(CONFIG_DEVICE_TYPE_LIGHTPANELS)
|| config.deviceType.equals(CONFIG_DEVICE_TYPE_CANVAS)) {
logger.trace("Anim Data rgb {} {} {} {}", panelID, red, green, blue);
write.setAnimData(String.format("1 %s 1 %d %d %d 0 10", panelID, red, green, blue));
} else {
// this is only used in special streaming situations with canvas which is not yet supported
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, red, green, blue));
}
write.setLoop(false);
effects.setWrite(write);
Request setNewRenderedEffectRequest = OpenAPIUtils.requestBuilder(httpClient, config, API_EFFECT,
HttpMethod.PUT);
String content = gson.toJson(effects);
logger.debug("sending effect command from panel {}: {}", getThing().getUID(), content);
setNewRenderedEffectRequest.content(new StringContentProvider(content), "application/json");
OpenAPIUtils.sendOpenAPIRequest(setNewRenderedEffectRequest);
} else {
logger.warn("Couldn't set rendering effect as Bridge-Handler {} is null", bridge.getUID());
}
}
}
public void updatePanelColorChannel() {
@Nullable
HSBType panelColor = getPanelColor();
logger.trace("updatePanelColorChannel: panelColor: {}", panelColor);
if (panelColor != null)
updateState(CHANNEL_PANEL_COLOR, panelColor);
}
/**
* Apply the gesture to the panel
*
* @param gesture Only 0=single tap and 1=double tap are supported
*/
public void updatePanelGesture(int gesture) {
switch (gesture) {
case 0:
updateState(CHANNEL_PANEL_SINGLE_TAP, OnOffType.ON);
singleTapJob = scheduler.schedule(this::resetSingleTap, 1, TimeUnit.SECONDS);
logger.debug("Asserting single tap of panel {} to ON", getPanelID());
break;
case 1:
updateState(CHANNEL_PANEL_DOUBLE_TAP, OnOffType.ON);
doubleTapJob = scheduler.schedule(this::resetDoubleTap, 1, TimeUnit.SECONDS);
logger.debug("Asserting double tap of panel {} to ON", getPanelID());
break;
}
}
private void resetSingleTap() {
updateState(CHANNEL_PANEL_SINGLE_TAP, OnOffType.OFF);
logger.debug("Resetting single tap of panel {} to OFF", getPanelID());
}
private void resetDoubleTap() {
updateState(CHANNEL_PANEL_DOUBLE_TAP, OnOffType.OFF);
logger.debug("Resetting double tap of panel {} to OFF", getPanelID());
}
public String getPanelID() {
String panelID = getThing().getConfiguration().get(CONFIG_PANEL_ID).toString();
return panelID;
}
private @Nullable HSBType getPanelColor() {
String panelID = getPanelID();
// get panel color data from controller
try {
Effects effects = new Effects();
Write write = new Write();
write.setCommand("request");
write.setAnimName("*Static*");
effects.setWrite(write);
Bridge bridge = getBridge();
if (bridge != null) {
NanoleafControllerHandler handler = (NanoleafControllerHandler) bridge.getHandler();
if (handler != null) {
NanoleafControllerConfig config = handler.getControllerConfig();
logger.debug("Sending Request from Panel for getColor()");
Request setPanelUpdateRequest = OpenAPIUtils.requestBuilder(httpClient, config, API_EFFECT,
HttpMethod.PUT);
setPanelUpdateRequest.content(new StringContentProvider(gson.toJson(effects)), "application/json");
ContentResponse panelData = OpenAPIUtils.sendOpenAPIRequest(setPanelUpdateRequest);
// parse panel data
parsePanelData(panelID, config, panelData);
}
}
} catch (NanoleafNotFoundException nfe) {
logger.warn("Panel data could not be retrieved as no data was returned (static type missing?) : {}",
nfe.getMessage());
} catch (NanoleafBadRequestException nfe) {
logger.debug(
"Panel data could not be retrieved as request not expected(static type missing / dynamic type on) : {}",
nfe.getMessage());
} catch (NanoleafException nue) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"@text/error.nanoleaf.panel.communication");
logger.warn("Panel data could not be retrieved: {}", nue.getMessage());
}
return panelInfo.get(panelID);
}
void parsePanelData(String panelID, NanoleafControllerConfig config, ContentResponse panelData) {
// panelData is in format (numPanels, (PanelId, 1, R, G, B, W, TransitionTime) * numPanel)
@Nullable
Write response = gson.fromJson(panelData.getContentAsString(), Write.class);
if (response != null) {
String[] tokenizedData = response.getAnimData().split(" ");
if (config.deviceType.equals(CONFIG_DEVICE_TYPE_LIGHTPANELS)
|| config.deviceType.equals(CONFIG_DEVICE_TYPE_CANVAS)) {
// panelData is in format (numPanels (PanelId 1 R G B W TransitionTime) * numPanel)
String[] panelDataPoints = Arrays.copyOfRange(tokenizedData, 1, tokenizedData.length);
for (int i = 0; i < panelDataPoints.length; i++) {
if (i % 7 == 0) {
String id = panelDataPoints[i];
if (id.equals(panelID)) {
// found panel data - store it
panelInfo.put(panelID,
HSBType.fromRGB(Integer.parseInt(panelDataPoints[i + 2]),
Integer.parseInt(panelDataPoints[i + 3]),
Integer.parseInt(panelDataPoints[i + 4])));
}
}
}
} else {
// panelData is in format (0 numPanels (quotient(panelID) remainder(panelID) R G B W 0
// quotient(TransitionTime) remainder(TransitionTime)) * numPanel)
String[] panelDataPoints = Arrays.copyOfRange(tokenizedData, 2, tokenizedData.length);
for (int i = 0; i < panelDataPoints.length; i++) {
if (i % 8 == 0) {
String idQuotient = panelDataPoints[i];
String idRemainder = panelDataPoints[i + 1];
Integer idNum = Integer.valueOf(idQuotient) * 256 + Integer.valueOf(idRemainder);
if (String.valueOf(idNum).equals(panelID)) {
// found panel data - store it
panelInfo.put(panelID,
HSBType.fromRGB(Integer.parseInt(panelDataPoints[i + 3]),
Integer.parseInt(panelDataPoints[i + 4]),
Integer.parseInt(panelDataPoints[i + 5])));
}
}
}
}
}
}
}

View File

@@ -0,0 +1,38 @@
/**
* Copyright (c) 2010-2020 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.nanoleaf.internal.model;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import com.google.gson.annotations.SerializedName;
/**
* Represents an Authorization Token
*
* @author Martin Raepple - Initial contribution
*/
@NonNullByDefault
public class AuthToken {
@SerializedName("auth_token")
private @Nullable String authToken;
public @Nullable String getAuthToken() {
return authToken;
}
public void setAuthToken(String authToken) {
this.authToken = authToken;
}
}

View File

@@ -0,0 +1,28 @@
/**
* Copyright (c) 2010-2020 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.nanoleaf.internal.model;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Interface for boolean value states
*
* @author Martin Raepple - Initial contribution
*/
@NonNullByDefault
public interface BooleanState {
boolean getValue();
void setValue(boolean value);
}

View File

@@ -0,0 +1,55 @@
/**
* Copyright (c) 2010-2020 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.nanoleaf.internal.model;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* Represents brightness setting of the light panels
*
* @author Martin Raepple - Initial contribution
*/
@NonNullByDefault
public class Brightness implements IntegerState {
private int value;
private @Nullable Integer max;
private @Nullable Integer min;
@Override
public int getValue() {
return value;
}
@Override
public void setValue(int value) {
this.value = value;
}
public @Nullable Integer getMax() {
return max;
}
public void setMax(Integer max) {
this.max = max;
}
public @Nullable Integer getMin() {
return min;
}
public void setMin(Integer min) {
this.min = min;
}
}

View File

@@ -0,0 +1,38 @@
/**
* Copyright (c) 2010-2020 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.nanoleaf.internal.model;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import com.google.gson.annotations.SerializedName;
/**
* Represents color temperature of the light panels
*
* @author Martin Raepple - Initial contribution
*/
@NonNullByDefault
public class Command {
@SerializedName("write")
private @Nullable Write write;
public @Nullable Write getWrite() {
return write;
}
public void setWrite(Write write) {
this.write = write;
}
}

View File

@@ -0,0 +1,106 @@
/**
* Copyright (c) 2010-2020 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.nanoleaf.internal.model;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Represents the light panels controller information
*
* @author Martin Raepple - Initial contribution
*/
@NonNullByDefault
public class ControllerInfo {
private String name = "";
private String serialNo = "";
private String manufacturer = "";
private String firmwareVersion = "";
private String model = "";
private State state = new State();
private Effects effects = new Effects();
private PanelLayout panelLayout = new PanelLayout();
private Rhythm rhythm = new Rhythm();
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getSerialNo() {
return serialNo;
}
public void setSerialNo(String serialNo) {
this.serialNo = serialNo;
}
public String getManufacturer() {
return manufacturer;
}
public void setManufacturer(String manufacturer) {
this.manufacturer = manufacturer;
}
public String getFirmwareVersion() {
return firmwareVersion;
}
public void setFirmwareVersion(String firmwareVersion) {
this.firmwareVersion = firmwareVersion;
}
public String getModel() {
return model;
}
public void setModel(String model) {
this.model = model;
}
public State getState() {
return state;
}
public void setState(State state) {
this.state = state;
}
public Effects getEffects() {
return effects;
}
public void setEffects(Effects effects) {
this.effects = effects;
}
public PanelLayout getPanelLayout() {
return panelLayout;
}
public void setPanelLayout(PanelLayout panelLayout) {
this.panelLayout = panelLayout;
}
public Rhythm getRhythm() {
return rhythm;
}
public void setRhythm(Rhythm rhythm) {
this.rhythm = rhythm;
}
}

View File

@@ -0,0 +1,55 @@
/**
* Copyright (c) 2010-2020 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.nanoleaf.internal.model;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* Represents color temperature of the light panels
*
* @author Martin Raepple - Initial contribution
*/
@NonNullByDefault
public class Ct implements IntegerState {
private int value;
private @Nullable Integer max;
private @Nullable Integer min;
public @Nullable Integer getMax() {
return max;
}
public void setMax(@Nullable Integer max) {
this.max = max;
}
public @Nullable Integer getMin() {
return min;
}
public void setMin(@Nullable Integer min) {
this.min = min;
}
@Override
public int getValue() {
return value;
}
@Override
public void setValue(int value) {
this.value = value;
}
}

View File

@@ -0,0 +1,55 @@
/**
* Copyright (c) 2010-2020 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.nanoleaf.internal.model;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* Represents effect commands for select and write
*
* @author Martin Raepple - Initial contribution
*/
@NonNullByDefault
public class Effects {
private @Nullable String select;
private @Nullable List<String> effectsList = null;
private @Nullable Write write;
public @Nullable String getSelect() {
return select;
}
public void setSelect(@Nullable String select) {
this.select = select;
}
public @Nullable List<String> getEffectsList() {
return effectsList;
}
public void setEffectsList(@Nullable List<String> effectsList) {
this.effectsList = effectsList;
}
public @Nullable Write getWrite() {
return write;
}
public void setWrite(@Nullable Write write) {
this.write = write;
}
}

View File

@@ -0,0 +1,53 @@
/**
* Copyright (c) 2010-2020 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.nanoleaf.internal.model;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* Represents global orientation settings of the light panels
*
* @author Martin Raepple - Initial contribution
*/
@NonNullByDefault
public class GlobalOrientation {
private int value;
private @Nullable Integer max;
private @Nullable Integer min;
public int getValue() {
return value;
}
public void setValue(int value) {
this.value = value;
}
public @Nullable Integer getMax() {
return max;
}
public void setMax(Integer max) {
this.max = max;
}
public @Nullable Integer getMin() {
return min;
}
public void setMin(Integer min) {
this.min = min;
}
}

View File

@@ -0,0 +1,55 @@
/**
* Copyright (c) 2010-2020 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.nanoleaf.internal.model;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* Represents hue setting of the light panels
*
* @author Martin Raepple - Initial contribution
*/
@NonNullByDefault
public class Hue implements IntegerState {
private int value;
private @Nullable Integer max;
private @Nullable Integer min;
@Override
public int getValue() {
return value;
}
@Override
public void setValue(int value) {
this.value = value;
}
public @Nullable Integer getMax() {
return max;
}
public void setMax(Integer max) {
this.max = max;
}
public @Nullable Integer getMin() {
return min;
}
public void setMin(Integer min) {
this.min = min;
}
}

View File

@@ -0,0 +1,28 @@
/**
* Copyright (c) 2010-2020 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.nanoleaf.internal.model;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Interface for settings with integer value
*
* @author Martin Raepple - Initial contribution
*/
@NonNullByDefault
public interface IntegerState {
void setValue(int value);
int getValue();
}

View File

@@ -0,0 +1,131 @@
/**
* Copyright (c) 2010-2020 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.nanoleaf.internal.model;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* Represents layout of the light panels
*
* @author Martin Raepple - Initial contribution
*/
@NonNullByDefault
public class Layout {
private int numPanels;
private int sideLength;
private @Nullable List<PositionDatum> positionData = null;
public int getNumPanels() {
return numPanels;
}
public void setNumPanels(int numPanels) {
this.numPanels = numPanels;
}
public int getSideLength() {
return sideLength;
}
public void setSideLength(int sideLength) {
this.sideLength = sideLength;
}
public @Nullable List<PositionDatum> getPositionData() {
return positionData;
}
public void setPositionData(List<PositionDatum> positionData) {
this.positionData = positionData;
}
/**
* Returns an text representation for a canvas layout.
*
* Note only canvas supported currently due to its easy geometry
*
* @return a String containing the layout
*/
public String getLayoutView() {
if (positionData != null) {
String view = "";
int minx = Integer.MAX_VALUE;
int maxx = Integer.MIN_VALUE;
int miny = Integer.MAX_VALUE;
int maxy = Integer.MIN_VALUE;
final int noofDefinedPanels = positionData.size();
for (int index = 0; index < noofDefinedPanels; index++) {
if (positionData != null) {
@Nullable
PositionDatum panel = positionData.get(index);
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();
}
}
}
}
int shiftWidth = getSideLength() / 2;
int lineY = maxy;
Map<Integer, PositionDatum> map;
while (lineY >= miny) {
map = new TreeMap<>();
for (int index = 0; index < noofDefinedPanels; index++) {
if (positionData != null) {
@Nullable
PositionDatum panel = positionData.get(index);
if (panel != null && panel.getPosY() == lineY)
map.put(panel.getPosX(), panel);
}
}
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());
} else
view += " ";
}
view += "\n";
}
return view;
} else
return "";
}
}

View File

@@ -0,0 +1,36 @@
/**
* Copyright (c) 2010-2020 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.nanoleaf.internal.model;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Represents power state of the light panels
*
* @author Martin Raepple - Initial contribution
*/
@NonNullByDefault
public class On implements BooleanState {
private boolean value;
@Override
public boolean getValue() {
return value;
}
@Override
public void setValue(boolean value) {
this.value = value;
}
}

View File

@@ -0,0 +1,52 @@
/**
* Copyright (c) 2010-2020 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.nanoleaf.internal.model;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Represents color palette in the write command
*
* @author Martin Raepple - Initial contribution
*/
@NonNullByDefault
public class Palette {
private int hue;
private int saturation;
private int brightness;
public int getHue() {
return hue;
}
public void setHue(int hue) {
this.hue = hue;
}
public int getSaturation() {
return saturation;
}
public void setSaturation(int saturation) {
this.saturation = saturation;
}
public int getBrightness() {
return brightness;
}
public void setBrightness(int brightness) {
this.brightness = brightness;
}
}

View File

@@ -0,0 +1,44 @@
/**
* Copyright (c) 2010-2020 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.nanoleaf.internal.model;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* Represents panel layout of the light panels
*
* @author Martin Raepple - Initial contribution
*/
@NonNullByDefault
public class PanelLayout {
private @Nullable Layout layout;
private @Nullable GlobalOrientation globalOrientation;
public @Nullable Layout getLayout() {
return layout;
}
public void setLayout(Layout layout) {
this.layout = layout;
}
public @Nullable GlobalOrientation getGlobalOrientation() {
return globalOrientation;
}
public void setGlobalOrientation(GlobalOrientation globalOrientation) {
this.globalOrientation = globalOrientation;
}
}

View File

@@ -0,0 +1,66 @@
/**
* Copyright (c) 2010-2020 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.nanoleaf.internal.model;
import org.eclipse.jdt.annotation.NonNullByDefault;
import com.google.gson.annotations.SerializedName;
/**
* Represents panel position
*
* @author Martin Raepple - Initial contribution
*/
@NonNullByDefault
public class PositionDatum {
private int panelId;
@SerializedName("x")
private int posX;
@SerializedName("y")
private int posY;
@SerializedName("o")
private int orientation;
public int getPanelId() {
return panelId;
}
public void setPanelId(int panelId) {
this.panelId = panelId;
}
public int getPosX() {
return posX;
}
public void setPosX(int x) {
this.posX = x;
}
public int getPosY() {
return posY;
}
public void setPosY(int y) {
this.posY = y;
}
public int getOrientation() {
return orientation;
}
public void setOrientation(int o) {
this.orientation = o;
}
}

View File

@@ -0,0 +1,98 @@
/**
* Copyright (c) 2010-2020 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.nanoleaf.internal.model;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* Represents rhythm module settings
*
* @author Martin Raepple - Initial contribution
*/
@NonNullByDefault
public class Rhythm {
private boolean rhythmConnected;
private boolean rhythmActive;
private int rhythmId;
private String hardwareVersion = "";
private String firmwareVersion = "";
private boolean auxAvailable;
private int rhythmMode;
private @Nullable RhythmPos rhythmPos;
public boolean getRhythmConnected() {
return rhythmConnected;
}
public void setRhythmConnected(boolean rhythmConnected) {
this.rhythmConnected = rhythmConnected;
}
public boolean getRhythmActive() {
return rhythmActive;
}
public void setRhythmActive(boolean rhythmActive) {
this.rhythmActive = rhythmActive;
}
public int getRhythmId() {
return rhythmId;
}
public void setRhythmId(int rhythmId) {
this.rhythmId = rhythmId;
}
public String getHardwareVersion() {
return this.hardwareVersion;
}
public void setHardwareVersion(String hardwareVersion) {
this.hardwareVersion = hardwareVersion;
}
public String getFirmwareVersion() {
return this.firmwareVersion;
}
public void setFirmwareVersion(String firmwareVersion) {
this.firmwareVersion = firmwareVersion;
}
public boolean getAuxAvailable() {
return auxAvailable;
}
public void setAuxAvailable(boolean auxAvailable) {
this.auxAvailable = auxAvailable;
}
public int getRhythmMode() {
return rhythmMode;
}
public void setRhythmMode(int rhythmMode) {
this.rhythmMode = rhythmMode;
}
public @Nullable RhythmPos getRhythmPos() {
return rhythmPos;
}
public void setRhythmPos(RhythmPos rhythmPos) {
this.rhythmPos = rhythmPos;
}
}

View File

@@ -0,0 +1,57 @@
/**
* Copyright (c) 2010-2020 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.nanoleaf.internal.model;
import org.eclipse.jdt.annotation.NonNullByDefault;
import com.google.gson.annotations.SerializedName;
/**
* Represents rhythm module position
*
* @author Martin Raepple - Initial contribution
*/
@NonNullByDefault
public class RhythmPos {
@SerializedName("x")
private float posX;
@SerializedName("y")
private float posY;
@SerializedName("o")
private float orientation;
public float getPosX() {
return posX;
}
public void setPosX(float x) {
this.posX = x;
}
public float getPosY() {
return posY;
}
public void setPosY(float y) {
this.posY = y;
}
public float getOrientation() {
return orientation;
}
public void setOrientation(float o) {
this.orientation = o;
}
}

View File

@@ -0,0 +1,55 @@
/**
* Copyright (c) 2010-2020 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.nanoleaf.internal.model;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* Represents saturation setting of the light panels
*
* @author Martin Raepple - Initial contribution
*/
@NonNullByDefault
public class Sat implements IntegerState {
private int value;
private @Nullable Integer max;
private @Nullable Integer min;
@Override
public int getValue() {
return value;
}
@Override
public void setValue(int value) {
this.value = value;
}
public @Nullable Integer getMax() {
return max;
}
public void setMax(Integer max) {
this.max = max;
}
public @Nullable Integer getMin() {
return min;
}
public void setMin(Integer min) {
this.min = min;
}
}

View File

@@ -0,0 +1,108 @@
/**
* Copyright (c) 2010-2020 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.nanoleaf.internal.model;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.library.types.OnOffType;
import com.google.gson.annotations.SerializedName;
/**
* Represents overall state settings of the light panels
*
* @author Martin Raepple - Initial contribution
*/
@NonNullByDefault
public class State {
private @Nullable On on;
private @Nullable Brightness brightness;
private @Nullable Hue hue;
@SerializedName("sat")
private @Nullable Sat saturation;
@SerializedName("ct")
private @Nullable Ct colorTemperature;
private @Nullable String colorMode;
public @Nullable On getOn() {
return on;
}
public OnOffType getOnOff() {
return (on != null && on.getValue()) ? OnOffType.ON : OnOffType.OFF;
}
public void setOn(On on) {
this.on = on;
}
public @Nullable Brightness getBrightness() {
return brightness;
}
public void setBrightness(Brightness brightness) {
this.brightness = brightness;
}
public @Nullable Hue getHue() {
return hue;
}
public void setHue(Hue hue) {
this.hue = hue;
}
public @Nullable Sat getSaturation() {
return saturation;
}
public void setSaturation(Sat sat) {
this.saturation = sat;
}
public @Nullable Ct getColorTemperature() {
return colorTemperature;
}
public void setColorTemperature(Ct ct) {
this.colorTemperature = ct;
}
public @Nullable String getColorMode() {
return colorMode;
}
public void setColorMode(String colorMode) {
this.colorMode = colorMode;
}
public void setState(IntegerState value) {
if (value instanceof Brightness) {
this.setBrightness((Brightness) value);
} else if (value instanceof Hue) {
this.setHue((Hue) value);
} else if (value instanceof Sat) {
this.setSaturation((Sat) value);
} else if (value instanceof Ct) {
this.setColorTemperature((Ct) value);
}
}
public void setState(BooleanState value) {
if (value instanceof On) {
this.setOn((On) value);
}
}
}

View File

@@ -0,0 +1,55 @@
/**
* Copyright (c) 2010-2020 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.nanoleaf.internal.model;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Nanoleaf Panel TouchEvent provided by controller
*
*
* JSON
* {"events":
* [
* { "panelId":48111,
* "gesture":0},
* { "panelId":48112,
* * "gesture":1}
* ]
* }
*
*
* @author Stefan Höhn - Initial contribution
*/
@NonNullByDefault
public class TouchEvent {
private String panelId = "";
private int gesture = -1;
public String getPanelId() {
return panelId;
}
public void setPanelId(String panelId) {
this.panelId = panelId;
}
public int getGesture() {
return gesture;
}
public void setGesture(int gesture) {
this.gesture = gesture;
}
}

View File

@@ -0,0 +1,38 @@
/**
* Copyright (c) 2010-2020 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.nanoleaf.internal.model;
import java.util.ArrayList;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Represents effect commands for select and write
*
* @author Stefan Höhn - Initial contribution
*/
@NonNullByDefault
public class TouchEvents {
private List<TouchEvent> events = new ArrayList<>();
public List<TouchEvent> getEvents() {
return events;
}
public void setEvents(List<TouchEvent> events) {
this.events = events;
}
}

View File

@@ -0,0 +1,91 @@
/**
* Copyright (c) 2010-2020 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.nanoleaf.internal.model;
import java.util.ArrayList;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Represents write command to set solid color effect
*
* @author Martin Raepple - Initial contribution
*/
@NonNullByDefault
public class Write {
private String command = "";
private String animType = "";
private String animName = "";
private List<Palette> palette = new ArrayList<>();
private String colorType = "";
private String animData = "";
private boolean loop = false;
public String getCommand() {
return command;
}
public void setCommand(String command) {
this.command = command;
}
public String getAnimType() {
return animType;
}
public void setAnimType(String animType) {
this.animType = animType;
}
public List<Palette> getPalette() {
return palette;
}
public void setPalette(List<Palette> palette) {
this.palette = palette;
}
public String getColorType() {
return colorType;
}
public void setColorType(String colorType) {
this.colorType = colorType;
}
public String getAnimData() {
return animData;
}
public void setAnimData(String animData) {
this.animData = animData;
}
public boolean getLoop() {
return loop;
}
public void setLoop(boolean loop) {
this.loop = loop;
}
public String getAnimName() {
return animName;
}
public void setAnimName(String animName) {
this.animName = animName;
}
}

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<binding:binding id="nanoleaf" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:binding="https://openhab.org/schemas/binding/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/binding/v1.0.0 https://openhab.org/schemas/binding-1.0.0.xsd">
<name>@text/binding.nanoleaf.name</name>
<description>@text/binding.nanoleaf.description</description>
<author>Martin Raepple, Stefan Höhn</author>
</binding:binding>

View File

@@ -0,0 +1,46 @@
<?xml version="1.0" encoding="UTF-8"?>
<config-description:config-descriptions
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0 https://openhab.org/schemas/config-description-1.0.0.xsd">
<config-description uri="thing-type:nanoleaf:controller">
<parameter name="address" type="text" required="true">
<context>network-address</context>
<label>@text/thing-type.config.nanoleaf.controller.address.label</label>
<description>@text/thing-type.config.nanoleaf.controller.address.description</description>
</parameter>
<parameter name="port" type="integer" required="true" min="1" max="65535">
<label>@text/thing-type.config.nanoleaf.controller.port.label</label>
<description>@text/thing-type.config.nanoleaf.controller.port.description</description>
<default>16021</default>
</parameter>
<parameter name="authToken" type="text">
<context>password</context>
<label>@text/thing-type.config.nanoleaf.controller.authToken.label</label>
<description>@text/thing-type.config.nanoleaf.controller.authToken.description</description>
</parameter>
<parameter name="refreshInterval" type="integer" unit="s">
<label>@text/thing-type.config.nanoleaf.controller.refreshInterval.label</label>
<description>@text/thing-type.config.nanoleaf.controller.refreshInterval.description</description>
<default>60</default>
</parameter>
<parameter name="deviceType" type="text" readOnly="true">
<label>@text/thing-type.config.nanoleaf.controller.deviceType.label</label>
<description>@text/thing-type.config.nanoleaf.controller.deviceType.description</description>
<default>lightPanels</default>
<options>
<option value="lightPanels">Light Panels</option>
<option value="canvas">Canvas</option>
</options>
</parameter>
</config-description>
<config-description uri="thing-type:nanoleaf:lightpanel">
<parameter name="id" type="integer" required="true">
<label>@text/thing-type.config.nanoleaf.lightpanel.id.label</label>
<description>@text/thing-type.config.nanoleaf.lightpanel.id.description</description>
</parameter>
</config-description>
</config-description:config-descriptions>

View File

@@ -0,0 +1,65 @@
binding.nanoleaf.name = Nanoleaf Binding
binding.nanoleaf.description = Integrates the Nanoleaf Light Panels (v150320)
# thing types
thing-type.nanoleaf.controller.name = Nanoleaf Controller
thing-type.nanoleaf.controller.description = The Nanoleaf controller (bridge) device
thing-type.nanoleaf.lightpanel.name = Nanoleaf Panel
thing-type.nanoleaf.lightpanel.description = A panel connected to the Nanoleaf controller
# config
thing-type.config.nanoleaf.controller.address.label = IP Address Or Hostname
thing-type.config.nanoleaf.controller.address.description = IP address or hostname of the Nanoleaf controller, for example 192.168.0.10
thing-type.config.nanoleaf.controller.port.label = Port
thing-type.config.nanoleaf.controller.port.description = Port number of the Nanoleaf API, for example 16021
thing-type.config.nanoleaf.controller.authToken.label = Authorization Token
thing-type.config.nanoleaf.controller.authToken.description = Authorization token, required by openHAB to call the controller API. For pairing, press the on-off button of the controller for 5-7 seconds until the LED starts flashing in a pattern.
thing-type.config.nanoleaf.controller.refreshInterval.label = Refresh Interval
thing-type.config.nanoleaf.controller.refreshInterval.description = Interval (in seconds) to refresh the controller channels status
thing-type.config.nanoleaf.controller.deviceType.label = Nanoleaf Device Type
thing-type.config.nanoleaf.controller.deviceType.description = Light Panels (triangles) or Canvas (squares)
thing-type.config.nanoleaf.lightpanel.id.label = ID Of The Panel
thing-type.config.nanoleaf.lightpanel.id.description = The ID of the panel assigned by the controller
# channel types
channel-type.nanoleaf.power.label = Power
channel-type.nanoleaf.power.description = Power state of the controller
channel-type.nanoleaf.color.label = Color
channel-type.nanoleaf.color.description = Color setting for an individual or all panels
channel-type.nanoleaf.colorTemperature.label = Color Temperature
channel-type.nanoleaf.colorTemperature.description = Color temperature in percent
channel-type.nanoleaf.colorTemperatureAbs.label = Color Temperature (K)
channel-type.nanoleaf.colorTemperatureAbs.description = Color temperature in Kelvin (K)
channel-type.nanoleaf.colorMode.label = Color Mode
channel-type.nanoleaf.colorMode.description = Current color mode: Effect, hue/saturation or color temperature
channel-type.nanoleaf.effect.label = Effect
channel-type.nanoleaf.effect.description = Effect or scene currently playing
channel-type.nanoleaf.rhythmState.label = Rhythm State
channel-type.nanoleaf.rhythmState.description = Connection state of the rhythm module
channel-type.nanoleaf.rhythmActive.label = Rhythm Active
channel-type.nanoleaf.rhythmActive.description = Activity state of the rhythm module
channel-type.nanoleaf.rhythmMode.label = Rhythm Mode
channel-type.nanoleaf.rhythmMode.description = Sound source for the rhythm module (microphone or aux cable)
channel-type.nanoleaf.panelLayout.label = Panel Layout
channel-type.nanoleaf.panelLayout.description = Creates a panel layout upon request
channel-type.nanoleaf.panelColor.label = Panel Color
channel-type.nanoleaf.panelColor.description = Color of the individual panel
channel-type.nanoleaf.singleTap.label = SingleTap
channel-type.nanoleaf.singleTap.description = Single tap on the panel
channel-type.nanoleaf.doubleTap.label = DoubleTap
channel-type.nanoleaf.doubleTap.description = Double tap on the panel
# error messages
error.nanoleaf.controller.noIp = IP/host address and/or port are not configured for the controller.
error.nanoleaf.controller.incompatibleFirmware = Incompatible controller firmware. Remove the device, update the firmware, and add it again.
error.nanoleaf.controller.noToken = No authorization token found. To start pairing, press the on-off button of the controller for 5-7 seconds until the LED starts flashing in a pattern.
error.nanoleaf.controller.invalidToken = Invalid token. Replace with valid token or start pairing again by removing the invalid token from the configuration.
error.nanoleaf.controller.communication = Communication failed. Please check configuration.
error.nanoleaf.controller.pairingFailed = Pairing failed. Press the on-off button for 5-7 seconds until the LED starts flashing in a pattern.
error.nanoleaf.controller.invalidData = Pairing failed. Received invalid data
error.nanoleaf.controller.noTokenReceived = Pairing failed. No authorization token received from controller.
error.nanoleaf.controller.authRequest = Pairing failed. Cannot send authorization request.
error.nanoleaf.controller.noClient = Pairing failed. Cannot start HTTP client.
error.nanoleaf.controller.runtime = Runtime error. See openHAB log for more details.
error.nanoleaf.panel.communication = Panel data could not be retrieved. Please check controller configuration.
error.nanoleaf.panel.controllerOffline = Controller is offline. Check configuration.

View File

@@ -0,0 +1,65 @@
binding.nanoleaf.name = Nanoleaf Binding
binding.nanoleaf.description = Binding für die Integration des Nanoleaf Light Panels (v150320)
# thing types
thing-type.nanoleaf.controller.name = Nanoleaf Controller
thing-type.nanoleaf.controller.description = Nanoleaf Controller (Brücke)
thing-type.nanoleaf.lightpanel.name = Nanoleaf Paneel
thing-type.nanoleaf.lightpanel.description = Ein mit dem Controller verbundenes Paneel
# config
thing-type.config.nanoleaf.controller.address.label = IP Adresse oder Hostname
thing-type.config.nanoleaf.controller.address.description = IP Adresse oder Hostname des Nanoleaf Controllers, z. B. 192.168.0.10
thing-type.config.nanoleaf.controller.port.label = Port
thing-type.config.nanoleaf.controller.port.description = Portnummer des Controllers, z. B. 16021
thing-type.config.nanoleaf.controller.authToken.label = Authentifizierungstoken
thing-type.config.nanoleaf.controller.authToken.description = Token zur Authentifizierung. Um openHAB mit dem Nanoleaf Light Panels zu verbinden (Pairing) den An/Aus-Schalter am Controller für 5-7 Sekunden gedrückthalten, bis die LED zu blinken beginnt.
thing-type.config.nanoleaf.controller.refreshInterval.label = Aktualisierungsintervall
thing-type.config.nanoleaf.controller.refreshInterval.description = Intervall (in Sekunden) in dem die Kanäle aktualisiert werden
thing-type.config.nanoleaf.controller.deviceType.label = Nanoleaf Gerätetyp
thing-type.config.nanoleaf.controller.deviceType.description = Light Panels (Dreiecke) oder Canvas (Quadrate)
thing-type.config.nanoleaf.lightpanel.id.label = Paneel ID
thing-type.config.nanoleaf.lightpanel.id.description = Vom Controller vergebene ID eines Paneels
# channel types
channel-type.nanoleaf.power.label = Power
channel-type.nanoleaf.power.description = Ermöglicht das An- und Ausschalten des Nanoleaf Light Panels
channel-type.nanoleaf.color.label = Farbe
channel-type.nanoleaf.color.description = Farbe aller oder eines einzelnen Paneels
channel-type.nanoleaf.colorTemperature.label = Farbtemperatur
channel-type.nanoleaf.colorTemperature.description = Farbtemperatur in Prozent
channel-type.nanoleaf.colorTemperatureAbs.label = Farbtemperatur (K)
channel-type.nanoleaf.colorTemperatureAbs.description = Farbtemperatur in Kelvin (K)
channel-type.nanoleaf.colorMode.label = Farbmodus
channel-type.nanoleaf.colorMode.description = Effekt, Hue/Sat oder Farbtemperatur für alle Paneele.
channel-type.nanoleaf.effect.label = Effekt
channel-type.nanoleaf.effect.description = Einstellung des Effektes
channel-type.nanoleaf.rhythmState.label = Rhythm Status
channel-type.nanoleaf.rhythmState.description = Anschlusszustand des Rhythm Moduls
channel-type.nanoleaf.rhythmActive.label = Rhythm Aktiv
channel-type.nanoleaf.rhythmActive.description = Zeigt an ob das Mikrofon des Rhythm Modules ativ ist.
channel-type.nanoleaf.rhythmMode.label = Rhythm Modus
channel-type.nanoleaf.rhythmMode.description = Erlaubt den Wechsel zwischen eingebautem Mikrofon und AUX-Kabel.
channel-type.nanoleaf.panelLayout.label = PanelLayout
channel-type.nanoleaf.panelLayout.description = Erzeugt auf Anfrage ein Panel-Layout
channel-type.nanoleaf.panelColor.label = Paneelfarbe
channel-type.nanoleaf.panelColor.description = Farbe des einzelnen Paneels
channel-type.nanoleaf.singleTap.label = Einzel-Tap
channel-type.nanoleaf.singleTap.description = Panel wurde einmal angetippt
channel-type.nanoleaf.doubleTap.label = Doppel-Tap
channel-type.nanoleaf.doubleTap.description = Panel wurde zweimal hintereinander angetippt
# error messages
error.nanoleaf.controller.noIp = IP/Host-Adresse und/oder Port sind für den Controller nicht konfiguriert.
error.nanoleaf.controller.incompatibleFirmware = Inkompatible Controller-Firmware. Firmware aktualisieren und das Gerät neu hinzufügen.
error.nanoleaf.controller.noToken = Kein Authentifizierungstoken gefunden. Um das Pairing zu starten, den An/Aus-Knopf am Controller für 5-7 Sekunden lang gedrückt halten, bis die LED anfängt zu blinken.
error.nanoleaf.controller.invalidToken = Ungültiges Authentifizierungstoken. Token ändern oder das Pairing neu starten durch Löschen des ungültigen Tokens.
error.nanoleaf.controller.communication = Kommunikationsfehler. Konfiguration des Controllers prüfen.
error.nanoleaf.controller.pairingFailed = Pairing fehlgeschlagen. Um das Pairing zu starten, den An/Aus-Knopf am Controller für 5-7 Sekunden lang gedrückt halten, bis die LED anfängt zu blinken.
error.nanoleaf.controller.invalidData = Pairing fehlgeschlagen. Ungültige Daten vom Controller empfangen.
error.nanoleaf.controller.noTokenReceived = Pairing fehlgeschlagen. Kein Authentifizierungstoken empfangen.
error.nanoleaf.controller.authRequest = Pairing fehlgeschlagen. Berechtigungsanfrage konnte nicht an den Controller gesendet werden.
error.nanoleaf.controller.noClient = Pairing fehlgeschlagen. HTTP Client nicht verfügbar.
error.nanoleaf.controller.runtime = Laufzeitfehler. Siehe openHAB Logdatei für mehr Details.
error.nanoleaf.panel.communication = Paneeldaten konnten nicht empfangen werden. Konfiguration des Controllers prüfen.
error.nanoleaf.panel.controllerOffline = Controller ist nicht erreichbar. Konfiguration prüfen.

View File

@@ -0,0 +1,152 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="nanoleaf"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<bridge-type id="controller">
<label>@text/thing-type.nanoleaf.controller.name</label>
<description>@text/thing-type.nanoleaf.controller.description</description>
<channels>
<channel id="power" typeId="power"/>
<channel id="color" typeId="color"/>
<channel id="colorTemperature" typeId="colorTemperature"/>
<channel id="colorTemperatureAbs" typeId="colorTemperatureAbs"/>
<channel id="colorMode" typeId="colorMode"/>
<channel id="effect" typeId="effect"/>
<channel id="rhythmState" typeId="rhythmState"/>
<channel id="rhythmActive" typeId="rhythmActive"/>
<channel id="rhythmMode" typeId="rhythmMode"/>
<channel id="panelLayout" typeId="panelLayout"/>
</channels>
<properties>
<property name="vendor"/>
<property name="serialNumber"/>
<property name="firmwareVersion"/>
<property name="modelId"/>
</properties>
<representation-property>address</representation-property>
<config-description-ref uri="thing-type:nanoleaf:controller"/>
</bridge-type>
<thing-type id="lightpanel">
<supported-bridge-type-refs>
<bridge-type-ref id="controller"/>
</supported-bridge-type-refs>
<label>@text/thing-type.nanoleaf.lightpanel.name</label>
<description>@text/thing-type.nanoleaf.lightpanel.description</description>
<channels>
<channel id="panelColor" typeId="color"/>
<channel id="singleTap" typeId="singleTap"/>
<channel id="doubleTap" typeId="doubleTap"/>
</channels>
<representation-property>id</representation-property>
<config-description-ref uri="thing-type:nanoleaf:lightpanel"/>
</thing-type>
<channel-type id="power">
<item-type>Switch</item-type>
<label>@text/channel-type.nanoleaf.power.label</label>
<description>@text/channel-type.nanoleaf.power.description</description>
<category>Switch</category>
<state readOnly="false">
<options>
<option value="ON">On</option>
<option value="OFF">Off</option>
</options>
</state>
</channel-type>
<channel-type id="colorTemperature">
<item-type>Dimmer</item-type>
<label>@text/channel-type.nanoleaf.colorTemperature.label</label>
<description>@text/channel-type.nanoleaf.colorTemperature.description</description>
<state min="0" max="100" step="1"/>
</channel-type>
<channel-type id="colorTemperatureAbs">
<item-type>Number</item-type>
<label>@text/channel-type.nanoleaf.colorTemperatureAbs.label</label>
<description>@text/channel-type.nanoleaf.colorTemperatureAbs.description</description>
<category>ColorLight</category>
<state min="1200" max="6500" step="100"/>
</channel-type>
<channel-type id="colorMode">
<item-type>String</item-type>
<label>@text/channel-type.nanoleaf.colorMode.label</label>
<description>@text/channel-type.nanoleaf.colorMode.description</description>
<state readOnly="true">
<options>
<option value="effect">Effect mode</option>
<option value="hs">Hue/Saturation</option>
<option value="ct">Color temperature</option>
</options>
</state>
</channel-type>
<channel-type id="color">
<item-type>Color</item-type>
<label>@text/channel-type.nanoleaf.color.label</label>
<description>@text/Color of the Light Panels</description>
</channel-type>
<channel-type id="effect">
<item-type>String</item-type>
<label>@text/channel-type.nanoleaf.effect.label</label>
<description>@text/channel-type.nanoleaf.effect.description</description>
</channel-type>
<channel-type id="rhythmState">
<item-type>Switch</item-type>
<label>@text/channel-type.nanoleaf.rhythmState.label</label>
<description>@text/channel-type.nanoleaf.rhythmState.description</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="rhythmActive">
<item-type>Switch</item-type>
<label>@text/channel-type.nanoleaf.rhythmActive.label</label>
<description>@text/channel-type.nanoleaf.rhythmActive.description</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="rhythmMode">
<item-type>Number</item-type>
<label>@text/channel-type.nanoleaf.rhythmMode.label</label>
<description>@text/channel-type.nanoleaf.rhythmMode.description</description>
<state min="0" max="1">
<options>
<option value="0">Microphone</option>
<option value="1">Aux cable</option>
</options>
</state>
</channel-type>
<channel-type id="singleTap">
<item-type>Switch</item-type>
<label>@text/channel-type.nanoleaf.singleTap.label</label>
<description>@text/channel-type.nanoleaf.singleTap.description</description>
<state readOnly="false"/>
</channel-type>
<channel-type id="doubleTap">
<item-type>Switch</item-type>
<label>@text/channel-type.nanoleaf.doubleTap.label</label>
<description>@text/channel-type.nanoleaf.doubleTap.description</description>
<state readOnly="false"/>
</channel-type>
<channel-type id="panelLayout">
<item-type>Switch</item-type>
<label>@text/channel-type.nanoleaf.panelLayout.label</label>
<description>@text/channel-type.nanoleaf.panelLayout.description</description>
<state readOnly="false"/>
</channel-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,75 @@
/**
* Copyright (c) 2010-2020 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.nanoleaf.internal;
import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.assertThat;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.junit.Before;
import org.junit.Test;
import org.openhab.binding.nanoleaf.internal.model.Layout;
import com.google.gson.Gson;
/**
* Test for the Layout
*
* @author Stefan Höhn - Initial contribution
*/
@NonNullByDefault
public class LayoutTest {
private final Gson gson = new Gson();
String layout1Json = "";
String layoutInconsistentPanelNoJson = "";
@Before
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
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}]}";
}
@Test
public void testTheRightLayoutView() {
@Nullable
Layout layout = gson.fromJson(layout1Json, 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")));
}
@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")));
}
}

View File

@@ -0,0 +1,50 @@
/**
* Copyright (c) 2010-2020 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.nanoleaf.internal;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.assertThat;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.junit.Test;
import org.openhab.binding.nanoleaf.internal.model.TouchEvent;
import org.openhab.binding.nanoleaf.internal.model.TouchEvents;
import com.google.gson.Gson;
/**
* Test for the TouchEvents
*
* @author Stefan Höhn - Initial contribution
*/
@NonNullByDefault
public class TouchTest {
private final Gson gson = new Gson();
@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));
@Nullable
TouchEvent touchEvent = touchEvents.getEvents().get(0);
assertThat(touchEvent.getPanelId(), is("48111"));
assertThat(touchEvent.getGesture(), is(1));
}
}

View File

@@ -0,0 +1,80 @@
/**
* Copyright (c) 2010-2020 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.nanoleaf.internal.handler;
import static java.nio.file.Files.*;
import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.assertThat;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.Before;
import org.junit.Test;
import org.openhab.binding.nanoleaf.internal.model.ControllerInfo;
import org.openhab.binding.nanoleaf.internal.model.State;
import org.openhab.core.library.types.OnOffType;
import com.google.gson.Gson;
/**
* Test for the Layout
*
* @author Stefan Höhn - Initial contribution
*/
@NonNullByDefault
public class NanoleafControllerHandlerTest {
private final Gson gson = new Gson();
private String controllerInfoJSON = "";
@Before
public void setup() {
}
@Test
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}";
ControllerInfo controllerInfo = gson.fromJson(controllerInfoJSON, ControllerInfo.class);
final State state = controllerInfo.getState();
assertThat(state, is(notNullValue()));
assertThat(state.getOnOff(), is(OnOffType.ON));
}
@Test
public void testStateOff() {
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);
final State state = controllerInfo.getState();
assertThat(state, is(notNullValue()));
assertThat(state.getOnOff(), is(OnOffType.OFF));
}
@Test
public void testStateOnMissing() {
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);
final State state = controllerInfo.getState();
assertThat(state, is(notNullValue()));
assertThat(state.getOnOff(), is(OnOffType.OFF));
}
}