[nanoleaf] Reimplement touch detection based on SSE, stabilize behavior, add swipe support (#11133)

* [nanoleaf] reimplement touch detection based on sse, stabilize behavior
* [nanoleaf] add swipe support
* [nanoleaf] add / tested full shapes support

Signed-off-by: Stefan Höhn <stefan@andreaundstefanhoehn.de>
This commit is contained in:
stefan-hoehn
2021-09-28 09:07:12 +02:00
committed by GitHub
parent 89ef91bad3
commit d3d1c7ae0a
23 changed files with 737 additions and 428 deletions

View File

@@ -42,6 +42,7 @@ public class NanoleafBindingConstants {
// Panel configuration settings
public static final String CONFIG_PANEL_ID = "id";
public static final String CONTROLLER_PANEL_ID = "-1";
// List of controller channels
public static final String CHANNEL_COLOR = "color";
@@ -52,6 +53,11 @@ public class NanoleafBindingConstants {
public static final String CHANNEL_RHYTHM_STATE = "rhythmState";
public static final String CHANNEL_RHYTHM_ACTIVE = "rhythmActive";
public static final String CHANNEL_RHYTHM_MODE = "rhythmMode";
public static final String CHANNEL_SWIPE = "swipe";
public static final String CHANNEL_SWIPE_EVENT_UP = "UP";
public static final String CHANNEL_SWIPE_EVENT_DOWN = "DOWN";
public static final String CHANNEL_SWIPE_EVENT_LEFT = "LEFT";
public static final String CHANNEL_SWIPE_EVENT_RIGHT = "RIGHT";
// List of light panel channels
public static final String CHANNEL_PANEL_COLOR = "color";

View File

@@ -12,8 +12,6 @@
*/
package org.openhab.binding.nanoleaf.internal;
import static org.openhab.binding.nanoleaf.internal.NanoleafBindingConstants.*;
import java.util.Collections;
import java.util.Set;
import java.util.stream.Collectors;
@@ -21,7 +19,6 @@ import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.openhab.binding.nanoleaf.internal.handler.NanoleafControllerHandler;
import org.openhab.binding.nanoleaf.internal.handler.NanoleafPanelHandler;
import org.openhab.core.io.net.http.HttpClientFactory;
@@ -48,35 +45,35 @@ import org.slf4j.LoggerFactory;
@Component(configurationPid = "binding.nanoleaf", service = ThingHandlerFactory.class)
public class NanoleafHandlerFactory extends BaseThingHandlerFactory {
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Collections
.unmodifiableSet(Stream.of(THING_TYPE_LIGHT_PANEL, THING_TYPE_CONTROLLER).collect(Collectors.toSet()));
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Collections.unmodifiableSet(
Stream.of(NanoleafBindingConstants.THING_TYPE_LIGHT_PANEL, NanoleafBindingConstants.THING_TYPE_CONTROLLER)
.collect(Collectors.toSet()));
private final Logger logger = LoggerFactory.getLogger(NanoleafHandlerFactory.class);
private final HttpClient httpClient;
private final HttpClientFactory httpClientFactory;
@Activate
public NanoleafHandlerFactory(@Reference final HttpClientFactory httpClientFactory) {
this.httpClient = httpClientFactory.getCommonHttpClient();
public NanoleafHandlerFactory(@Reference HttpClientFactory httpClientFactory) {
this.httpClientFactory = httpClientFactory;
}
@Override
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
}
@Override
protected @Nullable ThingHandler createHandler(Thing thing) {
@Nullable
protected ThingHandler createHandler(Thing thing) {
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
if (THING_TYPE_CONTROLLER.equals(thingTypeUID)) {
NanoleafControllerHandler handler = new NanoleafControllerHandler((Bridge) thing, httpClient);
if (NanoleafBindingConstants.THING_TYPE_CONTROLLER.equals(thingTypeUID)) {
NanoleafControllerHandler handler = new NanoleafControllerHandler((Bridge) thing, this.httpClientFactory);
logger.debug("Nanoleaf controller handler created.");
return handler;
} else if (THING_TYPE_LIGHT_PANEL.equals(thingTypeUID)) {
NanoleafPanelHandler handler = new NanoleafPanelHandler(thing, httpClient);
} else if (NanoleafBindingConstants.THING_TYPE_LIGHT_PANEL.equals(thingTypeUID)) {
NanoleafPanelHandler handler = new NanoleafPanelHandler(thing, this.httpClientFactory);
logger.debug("Nanoleaf panel handler created.");
return handler;
} else {
return null;
}
return null;
}
}

View File

@@ -12,7 +12,8 @@
*/
package org.openhab.binding.nanoleaf.internal;
import static org.openhab.binding.nanoleaf.internal.NanoleafBindingConstants.*;
import static org.openhab.binding.nanoleaf.internal.NanoleafBindingConstants.API_ADD_USER;
import static org.openhab.binding.nanoleaf.internal.NanoleafBindingConstants.API_V1_BASE_URL;
import java.net.URI;
import java.net.URISyntaxException;
@@ -45,20 +46,17 @@ import org.slf4j.LoggerFactory;
*/
@NonNullByDefault
public class OpenAPIUtils {
private static final Logger LOGGER = LoggerFactory.getLogger(OpenAPIUtils.class);
// Regular expression for firmware version
private static final Pattern FIRMWARE_VERSION_PATTERN = Pattern.compile("(\\d+)\\.(\\d+)\\.(\\d+)");
private static final Pattern FIRMWARE_VERSION_PATTERN_BETA = Pattern.compile("(\\d+)\\.(\\d+)\\.(\\d+)-(\\d+)");
private static final long CONNECT_TIMEOUT = 10L;
public static Request requestBuilder(HttpClient httpClient, NanoleafControllerConfig controllerConfig,
String apiOperation, HttpMethod method) throws NanoleafException {
URI requestURI = getUri(controllerConfig, apiOperation, null);
LOGGER.trace("RequestBuilder: Sending Request {}:{} {} ", requestURI.getHost(), requestURI.getPort(),
requestURI.getPath());
return httpClient.newRequest(requestURI).method(method).timeout(10, TimeUnit.SECONDS);
LOGGER.trace("RequestBuilder: Sending Request {}:{} {} \n op: {} method: {}", new Object[] {
requestURI.getHost(), requestURI.getPort(), requestURI.getPath(), apiOperation, method.toString() });
return httpClient.newRequest(requestURI).method(method).timeout(CONNECT_TIMEOUT, TimeUnit.SECONDS);
}
public static URI getUri(NanoleafControllerConfig controllerConfig, String apiOperation, @Nullable String query)
@@ -73,35 +71,33 @@ public class OpenAPIUtils {
path = String.format("%s%s", API_V1_BASE_URL, apiOperation);
} else {
String authToken = controllerConfig.authToken;
if (authToken != null) {
path = String.format("%s/%s%s", API_V1_BASE_URL, authToken, apiOperation);
} else {
if (authToken == null) {
throw new NanoleafUnauthorizedException("No authentication token found in configuration");
}
path = String.format("%s/%s%s", API_V1_BASE_URL, authToken, apiOperation);
}
URI requestURI;
try {
requestURI = new URI(HttpScheme.HTTP.asString(), null, address, port, path, query, null);
} catch (URISyntaxException use) {
URI requestURI = new URI(HttpScheme.HTTP.asString(), (String) null, address, port, path, query,
(String) null);
return requestURI;
} catch (URISyntaxException var8) {
LOGGER.warn("URI could not be parsed with path {}", path);
throw new NanoleafException("Wrong URI format for API request");
}
return requestURI;
}
public static ContentResponse sendOpenAPIRequest(Request request) throws NanoleafException {
try {
traceSendRequest(request);
ContentResponse openAPIResponse;
openAPIResponse = request.send();
ContentResponse openAPIResponse = request.send();
if (LOGGER.isTraceEnabled()) {
LOGGER.trace("API response from Nanoleaf controller: {}", openAPIResponse.getContentAsString());
}
LOGGER.debug("API response code: {}", openAPIResponse.getStatus());
int responseStatus = openAPIResponse.getStatus();
if (responseStatus == HttpStatus.OK_200 || responseStatus == HttpStatus.NO_CONTENT_204) {
return openAPIResponse;
} else {
if (responseStatus != HttpStatus.OK_200 && responseStatus != HttpStatus.NO_CONTENT_204) {
if (openAPIResponse.getStatus() == HttpStatus.UNAUTHORIZED_401) {
throw new NanoleafUnauthorizedException("OpenAPI request unauthorized");
} else if (openAPIResponse.getStatus() == HttpStatus.NOT_FOUND_404) {
@@ -114,60 +110,67 @@ public class OpenAPIUtils {
throw new NanoleafException(String.format("OpenAPI request failed. HTTP response code %s",
openAPIResponse.getStatus()));
}
} else {
return openAPIResponse;
}
} catch (ExecutionException | TimeoutException clientException) {
if (clientException.getCause() instanceof HttpResponseException
&& ((HttpResponseException) clientException.getCause()).getResponse()
.getStatus() == HttpStatus.UNAUTHORIZED_401) {
} catch (ExecutionException ee) {
Throwable cause = ee.getCause();
if (cause != null && cause instanceof HttpResponseException
&& ((HttpResponseException) cause).getResponse().getStatus() == HttpStatus.UNAUTHORIZED_401) {
LOGGER.warn("OpenAPI request unauthorized. Invalid authorization token.");
throw new NanoleafUnauthorizedException("Invalid authorization token");
} else {
throw new NanoleafException("Failed to send OpenAPI request (final)", ee);
}
throw new NanoleafException("Failed to send OpenAPI request", clientException);
} catch (InterruptedException interruptedException) {
throw new NanoleafInterruptedException("OpenAPI request has been interrupted", interruptedException);
} catch (TimeoutException te) {
LOGGER.warn("OpenAPI request failed with timeout", te);
throw new NanoleafException("Failed to send OpenAPI request: Timeout", te);
} catch (InterruptedException ie) {
throw new NanoleafInterruptedException("OpenAPI request has been interrupted", ie);
}
}
private static void traceSendRequest(Request request) {
if (!LOGGER.isTraceEnabled()) {
return;
}
LOGGER.trace("Sending Request {} {}", request.getURI(),
request.getQuery() == null ? "no query parameters" : request.getQuery());
LOGGER.trace("Request method:{} uri:{} params{}\n", request.getMethod(), request.getURI(), request.getParams());
if (request.getContent() != null) {
Iterator<ByteBuffer> iter = request.getContent().iterator();
if (iter != null) {
if (LOGGER.isTraceEnabled()) {
LOGGER.trace("Sending Request {} {}", request.getURI(),
request.getQuery() == null ? "no query parameters" : request.getQuery());
LOGGER.trace("Request method:{} uri:{} params{}\n", request.getMethod(), request.getURI(),
request.getParams());
if (request.getContent() != null) {
Iterator<ByteBuffer> iter = request.getContent().iterator();
while (iter.hasNext()) {
@Nullable
ByteBuffer buffer = iter.next();
LOGGER.trace("Content {}", StandardCharsets.UTF_8.decode(buffer).toString());
}
}
}
}
public static boolean checkRequiredFirmware(@Nullable String modelId, @Nullable String currentFirmwareVersion) {
if (modelId == null || currentFirmwareVersion == null) {
if (modelId != null && currentFirmwareVersion != null) {
int[] currentVer = getFirmwareVersionNumbers(currentFirmwareVersion);
int[] requiredVer = getFirmwareVersionNumbers("NL22".equals(modelId) ? "1.5.0" : "1.1.0");
for (int i = 0; i < currentVer.length; ++i) {
if (currentVer[i] != requiredVer[i]) {
if (currentVer[i] > requiredVer[i]) {
return true;
}
return false;
}
}
return true;
} else {
return false;
}
int[] currentVer = getFirmwareVersionNumbers(currentFirmwareVersion);
int[] requiredVer = getFirmwareVersionNumbers(
MODEL_ID_LIGHTPANELS.equals(modelId) ? API_MIN_FW_VER_LIGHTPANELS : API_MIN_FW_VER_CANVAS);
for (int i = 0; i < currentVer.length; i++) {
if (currentVer[i] != requiredVer[i]) {
return currentVer[i] > requiredVer[i];
}
}
return true;
}
public static int[] getFirmwareVersionNumbers(String firmwareVersion) throws IllegalArgumentException {
LOGGER.debug("firmwareVersion: {}", firmwareVersion);
Matcher m = FIRMWARE_VERSION_PATTERN.matcher(firmwareVersion);
if (m.matches()) {
return new int[] { Integer.parseInt(m.group(1)), Integer.parseInt(m.group(2)),
Integer.parseInt(m.group(3)) };

View File

@@ -59,6 +59,10 @@ public class NanoleafCommandExtension extends AbstractConsoleCommandExtension {
ThingHandler handler = thing.getHandler();
if (handler instanceof NanoleafControllerHandler) {
NanoleafControllerHandler nanoleafControllerHandler = (NanoleafControllerHandler) handler;
if (!handler.getThing().isEnabled()) {
console.println(
"The following Nanoleaf is NOT enabled as a Thing. Enable it first to view its layout.");
}
String layout = nanoleafControllerHandler.getLayout();
console.println("Layout of Nanoleaf controller '" + thing.getUID().getAsString()
+ "' with label '" + thing.getLabel() + "':" + System.lineSeparator());

View File

@@ -15,7 +15,6 @@ package org.openhab.binding.nanoleaf.internal.commanddescription;
import java.util.List;
import java.util.stream.Collectors;
import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.nanoleaf.internal.NanoleafBindingConstants;
@@ -49,7 +48,11 @@ public class NanoleafCommandDescriptionProvider extends BaseDynamicCommandDescri
@Override
public void setThingHandler(ThingHandler handler) {
this.bridgeHandler = (NanoleafControllerHandler) handler;
bridgeHandler.registerControllerListener(this);
NanoleafControllerHandler localHandler = this.bridgeHandler;
if (localHandler != null) {
localHandler.registerControllerListener(this);
}
effectChannelUID = new ChannelUID(handler.getThing().getUID(), NanoleafBindingConstants.CHANNEL_EFFECT);
}
@@ -60,18 +63,19 @@ public class NanoleafCommandDescriptionProvider extends BaseDynamicCommandDescri
@Override
public void deactivate() {
if (bridgeHandler != null) {
bridgeHandler.unregisterControllerListener(this);
NanoleafControllerHandler localHandler = this.bridgeHandler;
if (localHandler != null) {
localHandler.unregisterControllerListener(this);
}
super.deactivate();
}
@Override
public void onControllerInfoFetched(@NonNull ThingUID bridge, @NonNull ControllerInfo controllerInfo) {
List<@NonNull String> effects = controllerInfo.getEffects().getEffectsList();
public void onControllerInfoFetched(ThingUID bridge, ControllerInfo controllerInfo) {
List<String> effects = controllerInfo.getEffects().getEffectsList();
ChannelUID uid = effectChannelUID;
if (effects != null && uid != null && uid.getThingUID().equals(bridge)) {
List<@NonNull CommandOption> commandOptions = effects.stream() //
List<CommandOption> commandOptions = effects.stream() //
.map(effect -> new CommandOption(effect, effect)) //
.collect(Collectors.toList());
setCommandOptions(uid, commandOptions);

View File

@@ -33,6 +33,7 @@ import org.openhab.core.config.discovery.AbstractDiscoveryService;
import org.openhab.core.config.discovery.DiscoveryResult;
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.thing.binding.BridgeHandler;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerService;
import org.slf4j.Logger;
@@ -64,8 +65,10 @@ public class NanoleafPanelsDiscoveryService extends AbstractDiscoveryService
@Override
public void deactivate() {
if (bridgeHandler != null) {
bridgeHandler.unregisterControllerListener(this);
NanoleafControllerHandler localBridgeHandler = bridgeHandler;
if (localBridgeHandler != null) {
Boolean result = localBridgeHandler.unregisterControllerListener(this);
logger.debug("unregistration of controller was {}", result ? "successful" : "unsuccessful");
}
super.deactivate();
}
@@ -89,13 +92,16 @@ public class NanoleafPanelsDiscoveryService extends AbstractDiscoveryService
private void createResultsFromControllerInfo() {
ThingUID bridgeUID;
if (bridgeHandler != null) {
bridgeUID = bridgeHandler.getThing().getUID();
BridgeHandler localBridgeHandler = bridgeHandler;
if (localBridgeHandler != null) {
bridgeUID = localBridgeHandler.getThing().getUID();
} else {
return;
}
if (controllerInfo != null) {
final PanelLayout panelLayout = controllerInfo.getPanelLayout();
ControllerInfo localControllerInfo = controllerInfo;
if (localControllerInfo != null) {
final PanelLayout panelLayout = localControllerInfo.getPanelLayout();
@Nullable
Layout layout = panelLayout.getLayout();
@@ -133,7 +139,9 @@ public class NanoleafPanelsDiscoveryService extends AbstractDiscoveryService
@Override
public void setThingHandler(ThingHandler handler) {
this.bridgeHandler = (NanoleafControllerHandler) handler;
this.bridgeHandler.registerControllerListener(this);
NanoleafControllerHandler localBridgeHandler = (NanoleafControllerHandler) handler;
localBridgeHandler.registerControllerListener(this);
}
@Override

View File

@@ -15,7 +15,6 @@ package org.openhab.binding.nanoleaf.internal.handler;
import static org.openhab.binding.nanoleaf.internal.NanoleafBindingConstants.*;
import java.net.URI;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.Collection;
import java.util.List;
@@ -33,11 +32,10 @@ import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.client.api.Response;
import org.eclipse.jetty.client.api.Result;
import org.eclipse.jetty.client.util.StringContentProvider;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.HttpStatus;
import org.openhab.binding.nanoleaf.internal.NanoleafBindingConstants;
import org.openhab.binding.nanoleaf.internal.NanoleafControllerListener;
import org.openhab.binding.nanoleaf.internal.NanoleafException;
import org.openhab.binding.nanoleaf.internal.NanoleafUnauthorizedException;
@@ -55,11 +53,13 @@ import org.openhab.binding.nanoleaf.internal.model.Hue;
import org.openhab.binding.nanoleaf.internal.model.IntegerState;
import org.openhab.binding.nanoleaf.internal.model.Layout;
import org.openhab.binding.nanoleaf.internal.model.On;
import org.openhab.binding.nanoleaf.internal.model.PanelLayout;
import org.openhab.binding.nanoleaf.internal.model.Rhythm;
import org.openhab.binding.nanoleaf.internal.model.Sat;
import org.openhab.binding.nanoleaf.internal.model.State;
import org.openhab.binding.nanoleaf.internal.model.TouchEvents;
import org.openhab.core.config.core.Configuration;
import org.openhab.core.io.net.http.HttpClientFactory;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.HSBType;
import org.openhab.core.library.types.IncreaseDecreaseType;
@@ -94,20 +94,21 @@ public class NanoleafControllerHandler extends BaseBridgeHandler {
// Pairing interval in seconds
private static final int PAIRING_INTERVAL = 10;
private static final int CONNECT_TIMEOUT = 10;
private final Logger logger = LoggerFactory.getLogger(NanoleafControllerHandler.class);
private HttpClientFactory httpClientFactory;
private HttpClient httpClient;
private List<NanoleafControllerListener> controllerListeners = new CopyOnWriteArrayList<>();
// Pairing, update and panel discovery jobs and touch event job
private @Nullable HttpClient httpClientSSETouchEvent;
private @Nullable Request sseTouchjobRequest;
private List<NanoleafControllerListener> controllerListeners = new CopyOnWriteArrayList<NanoleafControllerListener>();
private @NonNullByDefault({}) ScheduledFuture<?> pairingJob;
private @NonNullByDefault({}) ScheduledFuture<?> updateJob;
private @NonNullByDefault({}) ScheduledFuture<?> touchJob;
// JSON parser for API responses
private final Gson gson = new Gson();
// Controller configuration settings and channel values
private @Nullable String address;
private int port;
private int refreshIntervall;
@@ -115,12 +116,34 @@ public class NanoleafControllerHandler extends BaseBridgeHandler {
private @Nullable String deviceType;
private @NonNullByDefault({}) ControllerInfo controllerInfo;
public NanoleafControllerHandler(Bridge bridge, HttpClient httpClient) {
private boolean touchJobRunning = false;
public NanoleafControllerHandler(Bridge bridge, HttpClientFactory httpClientFactory) {
super(bridge);
this.httpClient = httpClient;
this.httpClientFactory = httpClientFactory;
this.httpClient = httpClientFactory.getCommonHttpClient();
}
private void initializeTouchHttpClient() {
String httpClientName = thing.getUID().getId();
try {
httpClientSSETouchEvent = httpClientFactory.createHttpClient(httpClientName);
final HttpClient localHttpClientSSETouchEvent = this.httpClientSSETouchEvent;
if (localHttpClientSSETouchEvent != null) {
localHttpClientSSETouchEvent.setConnectTimeout(CONNECT_TIMEOUT * 1000L);
localHttpClientSSETouchEvent.start();
}
} catch (Exception e) {
logger.error(
"Long running HttpClient for Nanoleaf controller handler {} cannot be started. Creating Handler failed.",
httpClientName);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
}
logger.debug("Using long SSE httpClient={} for {}}", httpClientSSETouchEvent, httpClientName);
}
@Override
public void initialize() {
logger.debug("Initializing the controller (bridge)");
updateStatus(ThingStatus.UNKNOWN);
@@ -128,42 +151,45 @@ public class NanoleafControllerHandler extends BaseBridgeHandler {
setAddress(config.address);
setPort(config.port);
setRefreshIntervall(config.refreshInterval);
setAuthToken(config.authToken);
String authToken = (config.authToken != null) ? config.authToken : "";
setAuthToken(authToken);
Map<String, String> properties = getThing().getProperties();
String propertyModelId = properties.get(Thing.PROPERTY_MODEL_ID);
if (hasTouchSupport(propertyModelId)) {
config.deviceType = DEVICE_TYPE_TOUCHSUPPORT;
initializeTouchHttpClient();
} else {
config.deviceType = DEVICE_TYPE_LIGHTPANELS;
}
setDeviceType(config.deviceType);
setDeviceType(config.deviceType);
String propertyFirmwareVersion = properties.get(Thing.PROPERTY_FIRMWARE_VERSION);
try {
if (config.address.isEmpty() || String.valueOf(config.port).isEmpty()) {
if (!config.address.isEmpty() && !String.valueOf(config.port).isEmpty()) {
if (propertyFirmwareVersion != null && !propertyFirmwareVersion.isEmpty() && !OpenAPIUtils
.checkRequiredFirmware(properties.get(Thing.PROPERTY_MODEL_ID), propertyFirmwareVersion)) {
logger.warn("Nanoleaf controller firmware is too old: {}. Must be equal or higher than {}",
propertyFirmwareVersion, API_MIN_FW_VER_LIGHTPANELS);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"@text/error.nanoleaf.controller.incompatibleFirmware");
stopAllJobs();
} else if (authToken != null && !authToken.isEmpty()) {
stopPairingJob();
startUpdateJob();
startTouchJob();
} else {
logger.debug("No token found. Start pairing background job");
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING,
"@text/error.nanoleaf.controller.noToken");
startPairingJob();
stopUpdateJob();
}
} else {
logger.warn("No IP address and port configured for the Nanoleaf controller");
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING,
"@text/error.nanoleaf.controller.noIp");
stopAllJobs();
} else if (propertyFirmwareVersion != null && !propertyFirmwareVersion.isEmpty() && !OpenAPIUtils
.checkRequiredFirmware(properties.get(Thing.PROPERTY_MODEL_ID), propertyFirmwareVersion)) {
logger.warn("Nanoleaf controller firmware is too old: {}. Must be equal or higher than {}",
propertyFirmwareVersion, API_MIN_FW_VER_LIGHTPANELS);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"@text/error.nanoleaf.controller.incompatibleFirmware");
stopAllJobs();
} else if (config.authToken == null || config.authToken.isEmpty()) {
logger.debug("No token found. Start pairing background job");
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING,
"@text/error.nanoleaf.controller.noToken");
startPairingJob();
stopUpdateJob();
} else {
stopPairingJob();
startUpdateJob();
startTouchJob();
}
} catch (IllegalArgumentException iae) {
logger.warn("Nanoleaf controller firmware version not in format x.y.z: {}",
@@ -173,55 +199,52 @@ public class NanoleafControllerHandler extends BaseBridgeHandler {
}
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
logger.debug("Received command {} for channel {}", command, channelUID);
if (!ThingStatus.ONLINE.equals(getThing().getStatusInfo().getStatus())) {
logger.debug("Cannot handle command. Bridge is not online.");
return;
}
try {
if (command instanceof RefreshType) {
updateFromControllerInfo();
} else {
switch (channelUID.getId()) {
case CHANNEL_COLOR:
case CHANNEL_COLOR_TEMPERATURE:
case CHANNEL_COLOR_TEMPERATURE_ABS:
sendStateCommand(channelUID.getId(), command);
break;
case CHANNEL_EFFECT:
sendEffectCommand(command);
break;
case CHANNEL_RHYTHM_MODE:
sendRhythmCommand(command);
break;
default:
logger.warn("Channel with id {} not handled", channelUID.getId());
break;
} else {
try {
if (command instanceof RefreshType) {
updateFromControllerInfo();
} else {
switch (channelUID.getId()) {
case CHANNEL_COLOR:
case CHANNEL_COLOR_TEMPERATURE:
case CHANNEL_COLOR_TEMPERATURE_ABS:
sendStateCommand(channelUID.getId(), command);
break;
case CHANNEL_EFFECT:
sendEffectCommand(command);
break;
case CHANNEL_RHYTHM_MODE:
sendRhythmCommand(command);
break;
default:
logger.warn("Channel with id {} not handled", channelUID.getId());
break;
}
}
} catch (NanoleafUnauthorizedException nue) {
logger.debug("Authorization for command {} to channelUID {} failed: {}", command, channelUID,
nue.getMessage());
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"@text/error.nanoleaf.controller.invalidToken");
} catch (NanoleafException ne) {
logger.debug("Handling command {} to channelUID {} failed: {}", command, channelUID, ne.getMessage());
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"@text/error.nanoleaf.controller.communication");
}
} catch (NanoleafUnauthorizedException nae) {
logger.warn("Authorization for command {} to channelUID {} failed: {}", command, channelUID,
nae.getMessage());
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"@text/error.nanoleaf.controller.invalidToken");
} catch (NanoleafException ne) {
logger.warn("Handling command {} to channelUID {} failed: {}", command, channelUID, ne.getMessage());
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"@text/error.nanoleaf.controller.communication");
}
}
@Override
public void handleRemoval() {
scheduler.execute(() -> {
// delete token for openHAB
ContentResponse deleteTokenResponse;
try {
Request deleteTokenRequest = OpenAPIUtils.requestBuilder(httpClient, getControllerConfig(),
API_DELETE_USER, HttpMethod.DELETE);
deleteTokenResponse = OpenAPIUtils.sendOpenAPIRequest(deleteTokenRequest);
ContentResponse deleteTokenResponse = OpenAPIUtils.sendOpenAPIRequest(deleteTokenRequest);
if (deleteTokenResponse.getStatus() != HttpStatus.NO_CONTENT_204) {
logger.warn("Failed to delete token for openHAB. Response code is {}",
deleteTokenResponse.getStatus());
@@ -272,32 +295,38 @@ public class NanoleafControllerHandler extends BaseBridgeHandler {
}
public String getLayout() {
Layout layout = controllerInfo.getPanelLayout().getLayout();
String layoutView = (layout != null) ? layout.getLayoutView() : "";
String layoutView = "";
if (controllerInfo != null) {
PanelLayout panelLayout = controllerInfo.getPanelLayout();
Layout layout = panelLayout.getLayout();
layoutView = layout != null ? layout.getLayoutView() : "";
}
return layoutView;
}
public synchronized void startPairingJob() {
if (pairingJob == null || pairingJob.isCancelled()) {
logger.debug("Start pairing job, interval={} sec", PAIRING_INTERVAL);
pairingJob = scheduler.scheduleWithFixedDelay(this::runPairing, 0, PAIRING_INTERVAL, TimeUnit.SECONDS);
pairingJob = scheduler.scheduleWithFixedDelay(this::runPairing, 0L, PAIRING_INTERVAL, TimeUnit.SECONDS);
}
}
private synchronized void stopPairingJob() {
logger.debug("Stop pairing job {}", pairingJob != null ? pairingJob.isCancelled() : "pairing job = null");
if (pairingJob != null && !pairingJob.isCancelled()) {
logger.debug("Stop pairing job");
pairingJob.cancel(true);
this.pairingJob = null;
pairingJob = null;
logger.debug("Stopped pairing job");
}
}
private synchronized void startUpdateJob() {
String localAuthToken = getAuthToken();
final String localAuthToken = getAuthToken();
if (localAuthToken != null && !localAuthToken.isEmpty()) {
if (updateJob == null || updateJob.isCancelled()) {
logger.debug("Start controller status job, repeat every {} sec", getRefreshInterval());
updateJob = scheduler.scheduleWithFixedDelay(this::runUpdate, 0, getRefreshInterval(),
updateJob = scheduler.scheduleWithFixedDelay(this::runUpdate, 0L, getRefreshInterval(),
TimeUnit.SECONDS);
}
} else {
@@ -307,126 +336,146 @@ public class NanoleafControllerHandler extends BaseBridgeHandler {
}
private synchronized void stopUpdateJob() {
logger.debug("Stop update job {}", updateJob != null ? updateJob.isCancelled() : "update job = null");
if (updateJob != null && !updateJob.isCancelled()) {
logger.debug("Stop status job");
updateJob.cancel(true);
this.updateJob = null;
updateJob = null;
logger.debug("Stopped status job");
}
}
private synchronized void startTouchJob() {
NanoleafControllerConfig config = getConfigAs(NanoleafControllerConfig.class);
if (!config.deviceType.equals(DEVICE_TYPE_TOUCHSUPPORT)) {
logger.debug("NOT starting TouchJob for Panel {} because it has wrong device type '{}' vs required '{}'",
logger.debug(
"NOT starting TouchJob for Controller {} because it has wrong device type '{}' vs required '{}'",
this.getThing().getUID(), config.deviceType, DEVICE_TYPE_TOUCHSUPPORT);
return;
} else {
logger.debug("Starting TouchJob for Panel {}", this.getThing().getUID());
}
String localAuthToken = getAuthToken();
if (localAuthToken != null && !localAuthToken.isEmpty()) {
if (touchJob == null || touchJob.isCancelled()) {
logger.debug("Starting Touchjob now");
touchJob = scheduler.schedule(this::runTouchDetection, 0, TimeUnit.SECONDS);
logger.debug("Starting TouchJob for Controller {}", getThing().getUID());
final String localAuthToken = getAuthToken();
if (localAuthToken != null && !localAuthToken.isEmpty()) {
if (touchJob != null && !touchJob.isDone()) {
logger.trace("tj: tj={} already running touchJobRunning = {} cancelled={} done={}", touchJob,
touchJobRunning, touchJob == null ? null : touchJob.isCancelled(),
touchJob == null ? null : touchJob.isDone());
} else {
logger.debug("tj: Starting NEW touch job : tj={} touchJobRunning={} cancelled={} done={}",
touchJob, touchJobRunning, touchJob == null ? null : touchJob.isCancelled(),
touchJob == null ? null : touchJob.isDone());
touchJob = scheduler.scheduleWithFixedDelay(this::runTouchDetection, 0L, 1L, TimeUnit.SECONDS);
}
} else {
logger.error("starting TouchJob for Controller {} failed - missing token", getThing().getUID());
}
} else {
logger.error("starting TouchJob for Controller {} failed - missing token", this.getThing().getUID());
}
}
private synchronized void stopTouchJob() {
logger.debug("Stop touch job {}", touchJob != null ? touchJob.isCancelled() : "touchJob job = null");
if (touchJob != null) {
logger.trace("tj: touch job stopping for {} with client {}", thing.getUID(), httpClientSSETouchEvent);
final Request localSSERequest = sseTouchjobRequest;
if (localSSERequest != null) {
localSSERequest.abort(new NanoleafException("Touch detection stopped"));
}
if (!touchJob.isCancelled()) {
touchJob.cancel(true);
}
touchJob = null;
touchJobRunning = false;
logger.debug("tj: touch job stopped for {} with client {}", thing.getUID(), httpClientSSETouchEvent);
}
}
private boolean hasTouchSupport(@Nullable String deviceType) {
return (MODELS_WITH_TOUCHSUPPORT.contains(deviceType));
}
private synchronized void stopTouchJob() {
if (touchJob != null && !touchJob.isCancelled()) {
logger.debug("Stop touch job");
touchJob.cancel(true);
this.touchJob = null;
}
return NanoleafBindingConstants.MODELS_WITH_TOUCHSUPPORT.contains(deviceType);
}
private void runUpdate() {
logger.debug("Run update job");
try {
updateFromControllerInfo();
startTouchJob(); // if device type has changed, start touch detection.
startTouchJob();
updateStatus(ThingStatus.ONLINE);
} catch (NanoleafUnauthorizedException nae) {
logger.warn("Status update unauthorized: {}", nae.getMessage());
logger.debug("Status update unauthorized for controller {}: {}", getThing().getUID(), nae.getMessage());
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"@text/error.nanoleaf.controller.invalidToken");
String localAuthToken = getAuthToken();
final String localAuthToken = getAuthToken();
if (localAuthToken == null || localAuthToken.isEmpty()) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING,
"@text/error.nanoleaf.controller.noToken");
}
} catch (NanoleafException ne) {
logger.warn("Status update failed: {}", ne.getMessage());
logger.debug("Status update failed for controller {} : {}", getThing().getUID(), ne.getMessage());
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"@text/error.nanoleaf.controller.communication");
} catch (RuntimeException e) {
logger.warn("Update job failed", e);
logger.debug("Update job failed", e);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "@text/error.nanoleaf.controller.runtime");
}
}
private void runPairing() {
logger.debug("Run pairing job");
try {
String localAuthToken = getAuthToken();
final String localAuthToken = getAuthToken();
if (localAuthToken != null && !localAuthToken.isEmpty()) {
if (pairingJob != null) {
pairingJob.cancel(false);
}
logger.debug("Authentication token found. Canceling pairing job");
return;
}
ContentResponse authTokenResponse = OpenAPIUtils
.requestBuilder(httpClient, getControllerConfig(), API_ADD_USER, HttpMethod.POST)
.timeout(20, TimeUnit.SECONDS).send();
.timeout(20L, TimeUnit.SECONDS).send();
String authTokenResponseString = (authTokenResponse != null) ? authTokenResponse.getContentAsString() : "";
if (logger.isTraceEnabled()) {
logger.trace("Auth token response: {}", authTokenResponse.getContentAsString());
logger.trace("Auth token response: {}", authTokenResponseString);
}
if (authTokenResponse.getStatus() != HttpStatus.OK_200) {
logger.debug("Pairing pending for {}. Controller returns status code {}", this.getThing().getUID(),
if (authTokenResponse != null && authTokenResponse.getStatus() != HttpStatus.OK_200) {
logger.debug("Pairing pending for {}. Controller returns status code {}", getThing().getUID(),
authTokenResponse.getStatus());
} else {
// get auth token from response
AuthToken authTokenObject = gson.fromJson(authTokenResponse.getContentAsString(), AuthToken.class);
localAuthToken = authTokenObject.getAuthToken();
if (localAuthToken != null && !localAuthToken.isEmpty()) {
logger.debug("Pairing succeeded.");
// Update and save the auth token in the thing configuration
Configuration config = editConfiguration();
config.put(NanoleafControllerConfig.AUTH_TOKEN, localAuthToken);
updateConfiguration(config);
updateStatus(ThingStatus.ONLINE);
// Update local field
setAuthToken(localAuthToken);
stopPairingJob();
startUpdateJob();
startTouchJob();
} else {
logger.debug("No auth token found in response: {}", authTokenResponse.getContentAsString());
AuthToken authTokenObject = gson.fromJson(authTokenResponseString, AuthToken.class);
authTokenObject = (authTokenObject != null) ? authTokenObject : new AuthToken();
if (authTokenObject.getAuthToken().isEmpty()) {
logger.debug("No auth token found in response: {}", authTokenResponseString);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"@text/error.nanoleaf.controller.pairingFailed");
throw new NanoleafException(authTokenResponse.getContentAsString());
throw new NanoleafException(authTokenResponseString);
}
logger.debug("Pairing succeeded.");
Configuration config = editConfiguration();
config.put(NanoleafControllerConfig.AUTH_TOKEN, authTokenObject.getAuthToken());
updateConfiguration(config);
updateStatus(ThingStatus.ONLINE);
// Update local field
setAuthToken(authTokenObject.getAuthToken());
stopPairingJob();
startUpdateJob();
startTouchJob();
}
} catch (JsonSyntaxException e) {
logger.warn("Received invalid data", e);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"@text/error.nanoleaf.controller.invalidData");
} catch (NanoleafException e) {
} catch (NanoleafException ne) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"@text/error.nanoleaf.controller.noTokenReceived");
} catch (InterruptedException | ExecutionException | TimeoutException e) {
} catch (ExecutionException | TimeoutException | InterruptedException e) {
logger.debug("Cannot send authorization request to controller: ", e);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"@text/error.nanoleaf.controller.authRequest");
@@ -440,133 +489,159 @@ public class NanoleafControllerHandler extends BaseBridgeHandler {
}
}
/**
* This is based on the touch event detection described in https://forum.nanoleaf.me/docs/openapi#_842h3097vbgq
*/
private static boolean touchJobRunning = false;
private void runTouchDetection() {
if (touchJobRunning) {
logger.debug("touch job already running. quitting.");
return;
private synchronized void runTouchDetection() {
final HttpClient localhttpSSEClientTouchEvent = httpClientSSETouchEvent;
int eventHashcode = -1;
if (localhttpSSEClientTouchEvent != null) {
eventHashcode = localhttpSSEClientTouchEvent.hashCode();
}
try {
touchJobRunning = true;
URI eventUri = OpenAPIUtils.getUri(getControllerConfig(), API_EVENTS, "id=4");
logger.debug("touch job registered on: {}", eventUri.toString());
httpClient.newRequest(eventUri).send(new Response.Listener.Adapter() // request runs forever
{
@Override
public void onContent(@Nullable Response response, @Nullable ByteBuffer content) {
String s = StandardCharsets.UTF_8.decode(content).toString();
logger.trace("content {}", s);
if (touchJobRunning) {
logger.trace("tj: touch job {} touch job already running. quitting. {} controller {} with {}\",\n",
touchJob, eventHashcode, thing.getUID(), httpClientSSETouchEvent);
} else {
try {
URI eventUri = OpenAPIUtils.getUri(getControllerConfig(), API_EVENTS, "id=4");
logger.debug("tj: touch job request registering for {} with client {}", thing.getUID(),
httpClientSSETouchEvent);
touchJobRunning = true;
if (localhttpSSEClientTouchEvent != null) {
localhttpSSEClientTouchEvent.setIdleTimeout(CONNECT_TIMEOUT * 1000L);
sseTouchjobRequest = localhttpSSEClientTouchEvent.newRequest(eventUri);
final Request localSSETouchjobRequest = sseTouchjobRequest;
int requestHashCode = -1;
if (localSSETouchjobRequest != null) {
requestHashCode = localSSETouchjobRequest.hashCode();
Scanner eventContent = new Scanner(s);
while (eventContent.hasNextLine()) {
String line = eventContent.nextLine().trim();
// we don't expect anything than content id:4, so we do not check that but only care about the
// data part
if (line.startsWith("data:")) {
String json = line.substring(5).trim(); // supposed to be JSON
try {
TouchEvents touchEvents = gson.fromJson(json, TouchEvents.class);
handleTouchEvents(Objects.requireNonNull(touchEvents));
} catch (JsonSyntaxException jse) {
logger.error("couldn't parse touch event json {}", json);
logger.debug("tj: triggering new touch job request {} for {} with client {}", requestHashCode,
thing.getUID(), eventHashcode);
localSSETouchjobRequest.onResponseContent((response, content) -> {
String s = StandardCharsets.UTF_8.decode(content).toString();
logger.debug("touch detected for controller {}", thing.getUID());
logger.trace("content {}", s);
Scanner eventContent = new Scanner(s);
while (eventContent.hasNextLine()) {
String line = eventContent.nextLine().trim();
if (line.startsWith("data:")) {
String json = line.substring(5).trim();
try {
TouchEvents touchEvents = gson.fromJson(json, TouchEvents.class);
handleTouchEvents(Objects.requireNonNull(touchEvents));
} catch (JsonSyntaxException e) {
logger.error("Couldn't parse touch event json {}", json);
}
}
}
eventContent.close();
logger.debug("leaving touch onContent");
}).onResponseSuccess((response) -> {
logger.trace("tj: r={} touch event SUCCESS: {}", response.getRequest(), response);
}).onResponseFailure((response, failure) -> {
logger.trace("tj: r={} touch event FAILURE. Touchjob not running anymore for controller {}",
response.getRequest(), thing.getUID());
}).send((result) -> {
logger.trace(
"tj: r={} touch event COMPLETE. Touchjob not running anymore for controller {} failed: {} succeeded: {}",
result.getRequest(), thing.getUID(), result.isFailed(), result.isSucceeded());
touchJobRunning = false;
});
}
}
logger.trace("tj: started touch job request for {} with {} at {}", thing.getUID(),
httpClientSSETouchEvent, eventUri);
} catch (NanoleafException | RuntimeException e) {
logger.warn("tj: setting up TouchDetection failed for controller {} with {}\",\n", thing.getUID(),
httpClientSSETouchEvent);
logger.warn("tj: setting up TouchDetection failed with exception", e);
} finally {
logger.trace("tj: touch job {} started for new request {} controller {} with {}\",\n",
touchJob.hashCode(), eventHashcode, thing.getUID(), httpClientSSETouchEvent);
}
}
}
private void handleTouchEvents(TouchEvents touchEvents) {
touchEvents.getEvents().forEach((event) -> {
logger.info("panel: {} gesture id: {}", event.getPanelId(), event.getGesture());
// Swipes go to the controller, taps go to the individual panel
if (event.getPanelId().equals(CONTROLLER_PANEL_ID)) {
logger.debug("Triggering controller {} with gesture {}.", thing.getUID(), event.getGesture());
updateControllerGesture(event.getGesture());
} else {
getThing().getThings().forEach((child) -> {
NanoleafPanelHandler panelHandler = (NanoleafPanelHandler) child.getHandler();
if (panelHandler != null) {
logger.trace("Checking available panel -{}- versus event panel -{}-", panelHandler.getPanelID(),
event.getPanelId());
if (panelHandler.getPanelID().equals(event.getPanelId())) {
logger.debug("Panel {} found. Triggering item with gesture {}.", panelHandler.getPanelID(),
event.getGesture());
panelHandler.updatePanelGesture(event.getGesture());
}
}
eventContent.close();
logger.debug("leaving touch onContent");
super.onContent(response, content);
}
@Override
public void onSuccess(@Nullable Response response) {
logger.trace("touch event SUCCESS: {}", response);
}
@Override
public void onFailure(@Nullable Response response, @Nullable Throwable failure) {
logger.trace("touch event FAILURE: {}", response);
}
@Override
public void onComplete(@Nullable Result result) {
logger.trace("touch event COMPLETE: {}", result);
}
});
} catch (RuntimeException | NanoleafException e) {
logger.warn("setting up TouchDetection failed", e);
} finally {
touchJobRunning = false;
}
logger.debug("leaving run touch detection");
});
}
});
}
/**
* Interate over all gathered touch events and apply them to the panel they belong to
* Apply the swipe gesture to the controller
*
* @param touchEvents
* @param gesture Only swipes are supported on the complete nanoleaf panels
*/
private void handleTouchEvents(TouchEvents touchEvents) {
touchEvents.getEvents().forEach(event -> {
logger.info("panel: {} gesture id: {}", event.getPanelId(), event.getGesture());
// Iterate over all child things = all panels of that controller
this.getThing().getThings().forEach(child -> {
NanoleafPanelHandler panelHandler = (NanoleafPanelHandler) child.getHandler();
if (panelHandler != null) {
logger.trace("Checking available panel -{}- versus event panel -{}-", panelHandler.getPanelID(),
event.getPanelId());
if (panelHandler.getPanelID().equals(event.getPanelId())) {
logger.debug("Panel {} found. Triggering item with gesture {}.", panelHandler.getPanelID(),
event.getGesture());
panelHandler.updatePanelGesture(event.getGesture());
}
}
});
});
private void updateControllerGesture(int gesture) {
switch (gesture) {
case 2:
triggerChannel(CHANNEL_SWIPE, CHANNEL_SWIPE_EVENT_UP);
break;
case 3:
triggerChannel(CHANNEL_SWIPE, CHANNEL_SWIPE_EVENT_DOWN);
break;
case 4:
triggerChannel(CHANNEL_SWIPE, CHANNEL_SWIPE_EVENT_LEFT);
break;
case 5:
triggerChannel(CHANNEL_SWIPE, CHANNEL_SWIPE_EVENT_RIGHT);
break;
}
}
private void updateFromControllerInfo() throws NanoleafException {
logger.debug("Update channels for controller {}", thing.getUID());
this.controllerInfo = receiveControllerInfo();
final State state = controllerInfo.getState();
controllerInfo = receiveControllerInfo();
State state = controllerInfo.getState();
OnOffType powerState = state.getOnOff();
@Nullable
Ct colorTemperature = state.getColorTemperature();
float colorTempPercent = 0f;
float colorTempPercent = 0.0F;
int hue;
int saturation;
if (colorTemperature != null) {
updateState(CHANNEL_COLOR_TEMPERATURE_ABS, new DecimalType(colorTemperature.getValue()));
@Nullable
Integer min = colorTemperature.getMin();
int colorMin = (min == null) ? 0 : min;
@Nullable
hue = min == null ? 0 : min;
Integer max = colorTemperature.getMax();
int colorMax = (max == null) ? 0 : max;
colorTempPercent = (colorTemperature.getValue() - colorMin) / (colorMax - colorMin)
* PercentType.HUNDRED.intValue();
saturation = max == null ? 0 : max;
colorTempPercent = (float) ((colorTemperature.getValue() - hue) / (saturation - hue)
* PercentType.HUNDRED.intValue());
}
updateState(CHANNEL_COLOR_TEMPERATURE, new PercentType(Float.toString(colorTempPercent)));
updateState(CHANNEL_EFFECT, new StringType(controllerInfo.getEffects().getSelect()));
@Nullable
Hue stateHue = state.getHue();
int hue = (stateHue != null) ? stateHue.getValue() : 0;
@Nullable
hue = stateHue != null ? stateHue.getValue() : 0;
Sat stateSaturation = state.getSaturation();
int saturation = (stateSaturation != null) ? stateSaturation.getValue() : 0;
@Nullable
saturation = stateSaturation != null ? stateSaturation.getValue() : 0;
Brightness stateBrightness = state.getBrightness();
int brightness = (stateBrightness != null) ? stateBrightness.getValue() : 0;
int brightness = stateBrightness != null ? stateBrightness.getValue() : 0;
updateState(CHANNEL_COLOR, new HSBType(new DecimalType(hue), new PercentType(saturation),
new PercentType(powerState == OnOffType.ON ? brightness : 0)));
@@ -582,9 +657,7 @@ public class NanoleafControllerHandler extends BaseBridgeHandler {
properties.put(Thing.PROPERTY_MODEL_ID, controllerInfo.getModel());
properties.put(Thing.PROPERTY_VENDOR, controllerInfo.getManufacturer());
updateProperties(properties);
Configuration config = editConfiguration();
if (hasTouchSupport(controllerInfo.getModel())) {
config.put(NanoleafControllerConfig.DEVICE_TYPE, DEVICE_TYPE_TOUCHSUPPORT);
logger.debug("Set to device type {}", DEVICE_TYPE_TOUCHSUPPORT);
@@ -603,7 +676,7 @@ public class NanoleafControllerHandler extends BaseBridgeHandler {
});
// update the color channels of each panel
this.getThing().getThings().forEach(child -> {
getThing().getThings().forEach(child -> {
NanoleafPanelHandler panelHandler = (NanoleafPanelHandler) child.getHandler();
if (panelHandler != null) {
logger.debug("Update color channel for panel {}", panelHandler.getThing().getUID());
@@ -653,8 +726,8 @@ public class NanoleafControllerHandler extends BaseBridgeHandler {
if (controllerInfo != null) {
@Nullable
Brightness brightness = controllerInfo.getState().getBrightness();
int brightnessMin = 0;
int brightnessMax = 0;
int brightnessMin;
int brightnessMax;
if (brightness != null) {
@Nullable
Integer min = brightness.getMin();
@@ -679,7 +752,7 @@ public class NanoleafControllerHandler extends BaseBridgeHandler {
}
}
} else {
logger.warn("Unhandled command type: {}", command.getClass().getName());
logger.warn("Unhandled command {} with command type: {}", command, command.getClass().getName());
return;
}
break;
@@ -736,30 +809,28 @@ public class NanoleafControllerHandler extends BaseBridgeHandler {
Effects effects = new Effects();
if (command instanceof StringType) {
effects.setSelect(command.toString());
Request setNewEffectRequest = OpenAPIUtils.requestBuilder(httpClient, getControllerConfig(), API_EFFECT,
HttpMethod.PUT);
String content = gson.toJson(effects);
logger.debug("sending effect command from controller {}: {}", getThing().getUID(), content);
setNewEffectRequest.content(new StringContentProvider(content), "application/json");
OpenAPIUtils.sendOpenAPIRequest(setNewEffectRequest);
} else {
logger.warn("Unhandled command type: {}", command.getClass().getName());
return;
}
Request setNewEffectRequest = OpenAPIUtils.requestBuilder(httpClient, getControllerConfig(), API_EFFECT,
HttpMethod.PUT);
String content = gson.toJson(effects);
logger.debug("sending effect command from controller {}: {}", getThing().getUID(), content);
setNewEffectRequest.content(new StringContentProvider(content), "application/json");
OpenAPIUtils.sendOpenAPIRequest(setNewEffectRequest);
}
private void sendRhythmCommand(Command command) throws NanoleafException {
Rhythm rhythm = new Rhythm();
if (command instanceof DecimalType) {
rhythm.setRhythmMode(((DecimalType) command).intValue());
Request setNewRhythmRequest = OpenAPIUtils.requestBuilder(httpClient, getControllerConfig(),
API_RHYTHM_MODE, HttpMethod.PUT);
setNewRhythmRequest.content(new StringContentProvider(gson.toJson(rhythm)), "application/json");
OpenAPIUtils.sendOpenAPIRequest(setNewRhythmRequest);
} else {
logger.warn("Unhandled command type: {}", command.getClass().getName());
return;
}
Request setNewRhythmRequest = OpenAPIUtils.requestBuilder(httpClient, getControllerConfig(), API_RHYTHM_MODE,
HttpMethod.PUT);
setNewRhythmRequest.content(new StringContentProvider(gson.toJson(rhythm)), "application/json");
OpenAPIUtils.sendOpenAPIRequest(setNewRhythmRequest);
}
private @Nullable String getAddress() {
@@ -786,7 +857,8 @@ public class NanoleafControllerHandler extends BaseBridgeHandler {
this.refreshIntervall = refreshIntervall;
}
private @Nullable String getAuthToken() {
@Nullable
private String getAuthToken() {
return authToken;
}
@@ -794,7 +866,8 @@ public class NanoleafControllerHandler extends BaseBridgeHandler {
this.authToken = authToken;
}
private @Nullable String getDeviceType() {
@Nullable
private String getDeviceType() {
return deviceType;
}

View File

@@ -36,6 +36,7 @@ import org.openhab.binding.nanoleaf.internal.OpenAPIUtils;
import org.openhab.binding.nanoleaf.internal.config.NanoleafControllerConfig;
import org.openhab.binding.nanoleaf.internal.model.Effects;
import org.openhab.binding.nanoleaf.internal.model.Write;
import org.openhab.core.io.net.http.HttpClientFactory;
import org.openhab.core.library.types.HSBType;
import org.openhab.core.library.types.IncreaseDecreaseType;
import org.openhab.core.library.types.OnOffType;
@@ -81,9 +82,9 @@ public class NanoleafPanelHandler extends BaseThingHandler {
private @NonNullByDefault({}) ScheduledFuture<?> singleTapJob;
private @NonNullByDefault({}) ScheduledFuture<?> doubleTapJob;
public NanoleafPanelHandler(Thing thing, HttpClient httpClient) {
public NanoleafPanelHandler(Thing thing, HttpClientFactory httpClientFactory) {
super(thing);
this.httpClient = httpClient;
this.httpClient = httpClientFactory.getCommonHttpClient();
}
@Override

View File

@@ -13,7 +13,6 @@
package org.openhab.binding.nanoleaf.internal.model;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import com.google.gson.annotations.SerializedName;
@@ -26,9 +25,9 @@ import com.google.gson.annotations.SerializedName;
public class AuthToken {
@SerializedName("auth_token")
private @Nullable String authToken;
private String authToken = "";
public @Nullable String getAuthToken() {
public String getAuthToken() {
return authToken;
}

View File

@@ -18,17 +18,21 @@ import java.util.TreeMap;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Represents layout of the light panels
*
* @author Martin Raepple - Initial contribution
* @author Stefan Höhn - further improvements
*/
@NonNullByDefault
public class Layout {
private int numPanels;
private int sideLength;
private final Logger logger = LoggerFactory.getLogger(Layout.class);
private @Nullable List<PositionDatum> positionData = null;
@@ -40,14 +44,6 @@ public class Layout {
this.numPanels = numPanels;
}
public int getSideLength() {
return sideLength;
}
public void setSideLength(int sideLength) {
this.sideLength = sideLength;
}
public @Nullable List<PositionDatum> getPositionData() {
return positionData;
}
@@ -64,38 +60,46 @@ public class Layout {
* @return a String containing the layout
*/
public String getLayoutView() {
if (positionData != null) {
List<PositionDatum> localPositionData = positionData;
if (localPositionData != null) {
String view = "";
int minx = Integer.MAX_VALUE;
int maxx = Integer.MIN_VALUE;
int miny = Integer.MAX_VALUE;
int maxy = Integer.MIN_VALUE;
int sideLength = Integer.MIN_VALUE;
final int noofDefinedPanels = positionData.size();
final int noofDefinedPanels = localPositionData.size();
/*
* Since 5.0.0 sidelengths are panelspecific and not delivered per layout but only the individual panel.
* The only approximation we can do then is to derive the max-sidelength
* the other issue is that panel sidelength have become fix per paneltype which has to be retrieved in a
* hardcoded way.
*/
for (int index = 0; index < noofDefinedPanels; index++) {
if (positionData != null) {
@Nullable
PositionDatum panel = positionData.get(index);
PositionDatum panel = localPositionData.get(index);
logger.debug("Layout: Panel position data x={} y={}", panel.getPosX(), panel.getPosY());
if (panel != null) {
if (panel.getPosX() < minx) {
minx = panel.getPosX();
}
if (panel.getPosX() > maxx) {
maxx = panel.getPosX();
}
if (panel.getPosY() < miny) {
miny = panel.getPosY();
}
if (panel.getPosY() > maxy) {
maxy = panel.getPosY();
}
}
if (panel.getPosX() < minx) {
minx = panel.getPosX();
}
if (panel.getPosX() > maxx) {
maxx = panel.getPosX();
}
if (panel.getPosY() < miny) {
miny = panel.getPosY();
}
if (panel.getPosY() > maxy) {
maxy = panel.getPosY();
}
if (panel.getPanelSize() > sideLength) {
sideLength = panel.getPanelSize();
}
}
int shiftWidth = getSideLength() / 2;
int shiftWidth = sideLength / 2;
if (shiftWidth == 0) {
// seems we do not have squares here
@@ -109,11 +113,10 @@ public class Layout {
map = new TreeMap<>();
for (int index = 0; index < noofDefinedPanels; index++) {
if (positionData != null) {
@Nullable
PositionDatum panel = positionData.get(index);
if (localPositionData != null) {
PositionDatum panel = localPositionData.get(index);
if (panel != null && panel.getPosY() == lineY) {
if (panel.getPosY() == lineY) {
map.put(panel.getPosX(), panel);
}
}
@@ -121,9 +124,13 @@ public class Layout {
lineY -= shiftWidth;
for (int x = minx; x <= maxx; x += shiftWidth) {
if (map.containsKey(x)) {
@Nullable
PositionDatum panel = map.get(x);
view += String.format("%5s ", panel.getPanelId());
if (panel != null) {
int panelId = panel.getPanelId();
view += String.format("%5s ", panelId);
} else {
view += " ";
}
} else {
view += " ";
}

View File

@@ -12,6 +12,9 @@
*/
package org.openhab.binding.nanoleaf.internal.model;
import java.util.HashMap;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import com.google.gson.annotations.SerializedName;
@@ -31,6 +34,25 @@ public class PositionDatum {
private int posY;
@SerializedName("o")
private int orientation;
@SerializedName("shapeType")
private int shapeType;
private static Map<Integer, Integer> panelSizes = new HashMap<Integer, Integer>();
public PositionDatum() {
// initialize constant sidelengths for panels. See https://forum.nanoleaf.me/docs chapter 3.3
if (panelSizes.isEmpty()) {
panelSizes.put(0, 150); // Triangle
panelSizes.put(1, 0); // Rhythm N/A
panelSizes.put(2, 100); // Square
panelSizes.put(3, 100); // Control Square Master
panelSizes.put(4, 100); // Control Square Passive
panelSizes.put(7, 67); // Hexagon
panelSizes.put(8, 134); // Triangle Shapes
panelSizes.put(9, 67); // Mini Triangle Shapes
panelSizes.put(12, 0); // Shapes Controller (N/A)
}
}
public int getPanelId() {
return panelId;
@@ -41,6 +63,9 @@ public class PositionDatum {
}
public int getPosX() {
if (getPanelSize() != 0 && posX % getPanelSize() == 99) { // hack: check the inaccuracy of 1
posX = (posX / getPanelSize() + 1) * getPanelSize();
}
return posX;
}
@@ -49,6 +74,13 @@ public class PositionDatum {
}
public int getPosY() {
// we need to fix the positions: see
// https://forum.nanoleaf.me/forum/aurora-open-api/squares-send-unprecise-layout-positions
// unfortunately this cannot be done in the setter as gson does not access setters
if (getPanelSize() != 0 && posY % getPanelSize() == 99) { // hack: check the inaccuracy of 1
posY = (posY / getPanelSize() + 1) * getPanelSize();
}
return posY;
}
@@ -63,4 +95,16 @@ public class PositionDatum {
public void setOrientation(int o) {
this.orientation = o;
}
public int getShapeType() {
return shapeType;
}
public void setShapeType(int shapeType) {
this.shapeType = shapeType;
}
public Integer getPanelSize() {
return panelSizes.getOrDefault(shapeType, 0);
}
}

View File

@@ -41,7 +41,8 @@ public class State {
}
public OnOffType getOnOff() {
return (on != null && on.getValue()) ? OnOffType.ON : OnOffType.OFF;
On localOn = on;
return (localOn != null && localOn.getValue()) ? OnOffType.ON : OnOffType.OFF;
}
public void setOn(On on) {

View File

@@ -16,11 +16,13 @@ import java.util.ArrayList;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* Represents write command to set solid color effect
*
* @author Martin Raepple - Initial contribution
* @author Stefan Höhn - Made colorType nullable
*/
@NonNullByDefault
public class Write {
@@ -29,7 +31,8 @@ public class Write {
private String animType = "";
private String animName = "";
private List<Palette> palette = new ArrayList<>();
private String colorType = "";
@Nullable
private String colorType; // is required to be null if not set!
private String animData = "";
private boolean loop = false;
@@ -57,7 +60,7 @@ public class Write {
this.palette = palette;
}
public String getColorType() {
public @Nullable String getColorType() {
return colorType;
}

View File

@@ -31,7 +31,7 @@
<default>lightPanels</default>
<options>
<option value="lightPanels">Light Panels</option>
<option value="canvas">Canvas</option>
<option value="canvas">Canvas/Shapes</option>
</options>
</parameter>
</config-description>

View File

@@ -36,6 +36,8 @@ channel-type.nanoleaf.panelColor.label = Panel Color
channel-type.nanoleaf.panelColor.description = Color of the individual panel
channel-type.nanoleaf.tap.label = Button
channel-type.nanoleaf.tap.description = Button events of the panel
channel-type.nanoleaf.swipe.label = Swipe
channel-type.nanoleaf.swipe.description = Swipe over the panels
# error messages
error.nanoleaf.controller.noIp = IP/host address and/or port are not configured for the controller.

View File

@@ -36,6 +36,8 @@ channel-type.nanoleaf.panelColor.label = Paneelfarbe
channel-type.nanoleaf.panelColor.description = Farbe des einzelnen Paneels
channel-type.nanoleaf.tap.label = Taster
channel-type.nanoleaf.tap.description = Tastevents des Panels
channel-type.nanoleaf.swipe.label = Wischen (Swipe)
channel-type.nanoleaf.swipe.description = Wischen (Swipe) über die Panels
# error messages
error.nanoleaf.controller.noIp = IP/Host-Adresse und/oder Port sind für den Controller nicht konfiguriert.

View File

@@ -17,6 +17,7 @@
<channel id="rhythmState" typeId="rhythmState"/>
<channel id="rhythmActive" typeId="rhythmActive"/>
<channel id="rhythmMode" typeId="rhythmMode"/>
<channel id="swipe" typeId="swipe"/>
</channels>
<properties>
@@ -92,4 +93,18 @@
</state>
</channel-type>
<channel-type id="swipe">
<kind>trigger</kind>
<label>@text/channel-type.nanoleaf.swipe.label</label>
<description>@text/channel-type.nanoleaf.swipe.description</description>
<event>
<options>
<option value="UP">Up</option>
<option value="DOWN">Down</option>
<option value="LEFT">Left</option>
<option value="RIGHT">Right</option>
</options>
</event>
</channel-type>
</thing:thing-descriptions>

View File

@@ -20,6 +20,7 @@ import org.eclipse.jdt.annotation.Nullable;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.openhab.binding.nanoleaf.internal.model.Layout;
import org.openhab.binding.nanoleaf.internal.model.Write;
import com.google.gson.Gson;
@@ -38,8 +39,36 @@ public class LayoutTest {
@BeforeEach
public void setup() {
layout1Json = "{\"numPanels\":14,\"sideLength\":100,\"positionData\":[{\"panelId\":41451,\"x\":350,\"y\":0,\"o\":0,\"shapeType\":3},{\"panelId\":8134,\"x\":350,\"y\":150,\"o\":0,\"shapeType\":2},{\"panelId\":58086,\"x\":200,\"y\":100,\"o\":270,\"shapeType\":2},{\"panelId\":38724,\"x\":300,\"y\":200,\"o\":0,\"shapeType\":2},{\"panelId\":48111,\"x\":200,\"y\":200,\"o\":270,\"shapeType\":2},{\"panelId\":56093,\"x\":100,\"y\":200,\"o\":0,\"shapeType\":2},{\"panelId\":55836,\"x\":0,\"y\":200,\"o\":0,\"shapeType\":2},{\"panelId\":31413,\"x\":100,\"y\":300,\"o\":90,\"shapeType\":2},{\"panelId\":9162,\"x\":300,\"y\":300,\"o\":90,\"shapeType\":2},{\"panelId\":13276,\"x\":400,\"y\":300,\"o\":90,\"shapeType\":2},{\"panelId\":17870,\"x\":400,\"y\":200,\"o\":0,\"shapeType\":2},{\"panelId\":5164,\"x\":500,\"y\":200,\"o\":0,\"shapeType\":2},{\"panelId\":64279,\"x\":600,\"y\":200,\"o\":0,\"shapeType\":2},{\"panelId\":39755,\"x\":500,\"y\":100,\"o\":90,\"shapeType\":2}]}";
// panel number is not consistent to returned panels in array but it should still work
layout1Json = "{\n" + " \"numPanels\": 14,\n" + " \"sideLength\": 0,\n"
+ " \"positionData\": [\n" + " {\n" + " \"panelId\": 60147,\n"
+ " \"x\": 199,\n" + " \"y\": 99,\n" + " \"o\": 0,\n"
+ " \"shapeType\": 3\n" + " },\n" + " {\n" + " \"panelId\": 61141,\n"
+ " \"x\": 200,\n" + " \"y\": 199,\n" + " \"o\": 90,\n"
+ " \"shapeType\": 2\n" + " },\n" + " {\n" + " \"panelId\": 42064,\n"
+ " \"x\": 100,\n" + " \"y\": 200,\n" + " \"o\": 180,\n"
+ " \"shapeType\": 2\n" + " },\n" + " {\n" + " \"panelId\": 186,\n"
+ " \"x\": 0,\n" + " \"y\": 200,\n" + " \"o\": 180,\n"
+ " \"shapeType\": 2\n" + " },\n" + " {\n" + " \"panelId\": 19209,\n"
+ " \"x\": 0,\n" + " \"y\": 100,\n" + " \"o\": 270,\n"
+ " \"shapeType\": 2\n" + " },\n" + " {\n" + " \"panelId\": 36604,\n"
+ " \"x\": 300,\n" + " \"y\": 99,\n" + " \"o\": 0,\n"
+ " \"shapeType\": 2\n" + " },\n" + " {\n" + " \"panelId\": 37121,\n"
+ " \"x\": 400,\n" + " \"y\": 99,\n" + " \"o\": 270,\n"
+ " \"shapeType\": 2\n" + " },\n" + " {\n" + " \"panelId\": 45187,\n"
+ " \"x\": 400,\n" + " \"y\": 199,\n" + " \"o\": 270,\n"
+ " \"shapeType\": 2\n" + " },\n" + " {\n" + " \"panelId\": 33626,\n"
+ " \"x\": 500,\n" + " \"y\": 199,\n" + " \"o\": 270,\n"
+ " \"shapeType\": 2\n" + " },\n" + " {\n" + " \"panelId\": 10523,\n"
+ " \"x\": 600,\n" + " \"y\": 199,\n" + " \"o\": 270,\n"
+ " \"shapeType\": 2\n" + " },\n" + " {\n" + " \"panelId\": 54086,\n"
+ " \"x\": 599,\n" + " \"y\": 99,\n" + " \"o\": 540,\n"
+ " \"shapeType\": 2\n" + " },\n" + " {\n" + " \"panelId\": 3512,\n"
+ " \"x\": 699,\n" + " \"y\": 99,\n" + " \"o\": 540,\n"
+ " \"shapeType\": 2\n" + " },\n" + " {\n" + " \"panelId\": 16398,\n"
+ " \"x\": 799,\n" + " \"y\": 99,\n" + " \"o\": 540,\n"
+ " \"shapeType\": 2\n" + " },\n" + " {\n" + " \"panelId\": 39163,\n"
+ " \"x\": 800,\n" + " \"y\": 199,\n" + " \"o\": 630,\n"
+ " \"shapeType\": 2\n" + " }\n" + " ]\n" + " }";
layoutInconsistentPanelNoJson = "{\"numPanels\":15,\"sideLength\":100,\"positionData\":[{\"panelId\":41451,\"x\":350,\"y\":0,\"o\":0,\"shapeType\":3},{\"panelId\":8134,\"x\":350,\"y\":150,\"o\":0,\"shapeType\":2},{\"panelId\":58086,\"x\":200,\"y\":100,\"o\":270,\"shapeType\":2},{\"panelId\":38724,\"x\":300,\"y\":200,\"o\":0,\"shapeType\":2},{\"panelId\":48111,\"x\":200,\"y\":200,\"o\":270,\"shapeType\":2},{\"panelId\":56093,\"x\":100,\"y\":200,\"o\":0,\"shapeType\":2},{\"panelId\":55836,\"x\":0,\"y\":200,\"o\":0,\"shapeType\":2},{\"panelId\":31413,\"x\":100,\"y\":300,\"o\":90,\"shapeType\":2},{\"panelId\":9162,\"x\":300,\"y\":300,\"o\":90,\"shapeType\":2},{\"panelId\":13276,\"x\":400,\"y\":300,\"o\":90,\"shapeType\":2},{\"panelId\":17870,\"x\":400,\"y\":200,\"o\":0,\"shapeType\":2},{\"panelId\":5164,\"x\":500,\"y\":200,\"o\":0,\"shapeType\":2},{\"panelId\":64279,\"x\":600,\"y\":200,\"o\":0,\"shapeType\":2},{\"panelId\":39755,\"x\":500,\"y\":100,\"o\":90,\"shapeType\":2}]}";
}
@@ -47,6 +76,23 @@ public class LayoutTest {
public void testTheRightLayoutView() {
@Nullable
Layout layout = gson.fromJson(layout1Json, Layout.class);
if (layout == null) {
layout = new Layout();
}
String layoutView = layout.getLayoutView();
assertThat(layoutView, is(equalTo(
" 186 42064 61141 45187 33626 10523 39163 \n"
+ " \n"
+ "19209 60147 36604 37121 54086 3512 16398 \n")));
}
@Test
public void testTheInconsistentLayoutView() {
@Nullable
Layout layout = gson.fromJson(layoutInconsistentPanelNoJson, Layout.class);
if (layout == null) {
layout = new Layout();
}
String layoutView = layout.getLayoutView();
assertThat(layoutView,
is(equalTo(" 31413 9162 13276 \n"
@@ -59,17 +105,17 @@ public class LayoutTest {
}
@Test
public void testTheInconsistentLayoutView() {
@Nullable
Layout layout = gson.fromJson(layoutInconsistentPanelNoJson, Layout.class);
String layoutView = layout.getLayoutView();
assertThat(layoutView,
is(equalTo(" 31413 9162 13276 \n"
+ " \n"
+ "55836 56093 48111 38724 17870 5164 64279 \n"
+ " 8134 \n"
+ " 58086 39755 \n"
+ " \n"
+ " 41451 \n")));
public void testEffects() {
Write write = new Write();
write.setCommand("display");
write.setAnimType("static");
write.setLoop(false);
int panelID = 123;
int quotient = Integer.divideUnsigned(Integer.valueOf(panelID), 256);
int remainder = Integer.remainderUnsigned(Integer.valueOf(panelID), 256);
write.setAnimData(String.format("0 1 %d %d %d %d %d 0 0 10", quotient, remainder, 20, 40, 60));
String content = gson.toJson(write);
assertThat(content, containsStringIgnoringCase("palette"));
assertThat(content, is(not(containsStringIgnoringCase("colorType"))));
}
}

View File

@@ -25,7 +25,7 @@ import org.junit.jupiter.api.Test;
*/
@NonNullByDefault
public class OpenAPUUtilsTest {
public class OpenAPIUtilsTest {
@Test
public void testStateOn() {

View File

@@ -16,6 +16,8 @@ import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.greaterThan;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.junit.jupiter.api.Test;
@@ -38,12 +40,16 @@ public class TouchTest {
@Test
public void testTheRightLayoutView() {
String json = "{\"events\":[{\"panelId\":48111,\"gesture\":1}]}";
@Nullable
TouchEvents touchEvents = gson.fromJson(json, TouchEvents.class);
assertThat(touchEvents.getEvents().size(), greaterThan(0));
assertThat(touchEvents.getEvents().size(), is(1));
if (touchEvents == null) {
touchEvents = new TouchEvents();
}
List<TouchEvent> events = touchEvents.getEvents();
assertThat(events.size(), greaterThan(0));
assertThat(events.size(), is(1));
@Nullable
TouchEvent touchEvent = touchEvents.getEvents().get(0);
TouchEvent touchEvent = events.get(0);
assertThat(touchEvent.getPanelId(), is("48111"));
assertThat(touchEvent.getGesture(), is(1));
}

View File

@@ -16,6 +16,7 @@ import static org.hamcrest.CoreMatchers.*;
import static org.hamcrest.MatcherAssert.assertThat;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.openhab.binding.nanoleaf.internal.model.ControllerInfo;
@@ -45,12 +46,15 @@ public class NanoleafControllerHandlerTest {
public void testStateOn() {
controllerInfoJSON = "{\r\n \"name\":\"Nanoleaf Light Panels 12:34:56\",\r\n \"serialNo\":\"S19082ABCDE\",\r\n \"manufacturer\":\"Nanoleaf\",\r\n \"firmwareVersion\":\"3.3.3\",\r\n \"hardwareVersion\":\"1.6-2\",\r\n \"model\":\"NL22\",\r\n \"cloudHash\":{\r\n\r\n },\r\n \"discovery\":{\r\n\r\n },\r\n \"effects\":{\r\n \"effectsList\":[\r\n \"Color Burst\",\r\n \"Fireworks\",\r\n \"Flames\",\r\n \"Forest\",\r\n \"Inner Peace\",\r\n \"Lightning\",\r\n \"Northern Lights\",\r\n \"Pulse Pop Beats\",\r\n \"Vibrant Sunrise\"\r\n ],\r\n \"select\":\"Flames\"\r\n },\r\n \"firmwareUpgrade\":{\r\n\r\n },\r\n \"panelLayout\":{\r\n \"globalOrientation\":{\r\n \"value\":0,\r\n \"max\":360,\r\n \"min\":0\r\n },\r\n \"layout\":{\r\n \"numPanels\":9,\r\n \"sideLength\":150,\r\n \"positionData\":[\r\n {\r\n \"panelId\":1,\r\n \"x\":299,\r\n \"y\":0,\r\n \"o\":300,\r\n \"shapeType\":0\r\n },\r\n {\r\n \"panelId\":2,\r\n \"x\":299,\r\n \"y\":86,\r\n \"o\":120,\r\n \"shapeType\":0\r\n },\r\n {\r\n \"panelId\":3,\r\n \"x\":224,\r\n \"y\":129,\r\n \"o\":60,\r\n \"shapeType\":0\r\n },\r\n {\r\n \"panelId\":4,\r\n \"x\":224,\r\n \"y\":216,\r\n \"o\":120,\r\n \"shapeType\":0\r\n },\r\n {\r\n \"panelId\":5,\r\n \"x\":149,\r\n \"y\":259,\r\n \"o\":60,\r\n \"shapeType\":0\r\n },\r\n {\r\n \"panelId\":6,\r\n \"x\":74,\r\n \"y\":216,\r\n \"o\":240,\r\n \"shapeType\":0\r\n },\r\n {\r\n \"panelId\":7,\r\n \"x\":0,\r\n \"y\":259,\r\n \"o\":60,\r\n \"shapeType\":0\r\n },\r\n {\r\n \"panelId\":8,\r\n \"x\":149,\r\n \"y\":346,\r\n \"o\":120,\r\n \"shapeType\":0\r\n },\r\n {\r\n \"panelId\":9,\r\n \"x\":374,\r\n \"y\":129,\r\n \"o\":180,\r\n \"shapeType\":0\r\n }\r\n ]\r\n }\r\n },\r\n \"rhythm\":{\r\n \"auxAvailable\":false,\r\n \"firmwareVersion\":\"2.4.3\",\r\n \"hardwareVersion\":\"2.0\",\r\n \"rhythmActive\":false,\r\n \"rhythmConnected\":true,\r\n \"rhythmId\":10,\r\n \"rhythmMode\":0,\r\n \"rhythmPos\":{\r\n \"x\":449.99521692839559,\r\n \"y\":86.60030339609753,\r\n \"o\":0.0\r\n }\r\n },\r\n \"schedules\":{\r\n\r\n },\r\n \"state\":{\r\n \"brightness\":{\r\n \"value\":29,\r\n \"max\":100,\r\n \"min\":0\r\n },\r\n \"colorMode\":\"effect\",\r\n \"ct\":{\r\n \"value\":3000,\r\n \"max\":6500,\r\n \"min\":1200\r\n },\r\n \"hue\":{\r\n \"value\":0,\r\n \"max\":360,\r\n \"min\":0\r\n },\r\n \"on\":{\r\n \"value\":true\r\n },\r\n \"sat\":{\r\n \"value\":0,\r\n \"max\":100,\r\n \"min\":0\r\n }\r\n }\r\n}";
@Nullable
ControllerInfo controllerInfo = gson.fromJson(controllerInfoJSON, ControllerInfo.class);
assertThat(controllerInfo, is(notNullValue()));
final State state = controllerInfo.getState();
assertThat(state, is(notNullValue()));
assertThat(state.getOnOff(), is(OnOffType.ON));
if (controllerInfo != null) {
final State state = controllerInfo.getState();
assertThat(state, is(notNullValue()));
assertThat(state.getOnOff(), is(OnOffType.ON));
}
}
@Test
@@ -58,11 +62,13 @@ public class NanoleafControllerHandlerTest {
controllerInfoJSON = "{\r\n \"name\":\"Nanoleaf Light Panels 12:34:56\",\r\n \"serialNo\":\"S19082ABCDE\",\r\n \"manufacturer\":\"Nanoleaf\",\r\n \"firmwareVersion\":\"3.3.3\",\r\n \"hardwareVersion\":\"1.6-2\",\r\n \"model\":\"NL22\",\r\n \"cloudHash\":{\r\n\r\n },\r\n \"discovery\":{\r\n\r\n },\r\n \"effects\":{\r\n \"effectsList\":[\r\n \"Color Burst\",\r\n \"Fireworks\",\r\n \"Flames\",\r\n \"Forest\",\r\n \"Inner Peace\",\r\n \"Lightning\",\r\n \"Northern Lights\",\r\n \"Pulse Pop Beats\",\r\n \"Vibrant Sunrise\"\r\n ],\r\n \"select\":\"Flames\"\r\n },\r\n \"firmwareUpgrade\":{\r\n\r\n },\r\n \"panelLayout\":{\r\n \"globalOrientation\":{\r\n \"value\":0,\r\n \"max\":360,\r\n \"min\":0\r\n },\r\n \"layout\":{\r\n \"numPanels\":9,\r\n \"sideLength\":150,\r\n \"positionData\":[\r\n {\r\n \"panelId\":1,\r\n \"x\":299,\r\n \"y\":0,\r\n \"o\":300,\r\n \"shapeType\":0\r\n },\r\n {\r\n \"panelId\":2,\r\n \"x\":299,\r\n \"y\":86,\r\n \"o\":120,\r\n \"shapeType\":0\r\n },\r\n {\r\n \"panelId\":3,\r\n \"x\":224,\r\n \"y\":129,\r\n \"o\":60,\r\n \"shapeType\":0\r\n },\r\n {\r\n \"panelId\":4,\r\n \"x\":224,\r\n \"y\":216,\r\n \"o\":120,\r\n \"shapeType\":0\r\n },\r\n {\r\n \"panelId\":5,\r\n \"x\":149,\r\n \"y\":259,\r\n \"o\":60,\r\n \"shapeType\":0\r\n },\r\n {\r\n \"panelId\":6,\r\n \"x\":74,\r\n \"y\":216,\r\n \"o\":240,\r\n \"shapeType\":0\r\n },\r\n {\r\n \"panelId\":7,\r\n \"x\":0,\r\n \"y\":259,\r\n \"o\":60,\r\n \"shapeType\":0\r\n },\r\n {\r\n \"panelId\":8,\r\n \"x\":149,\r\n \"y\":346,\r\n \"o\":120,\r\n \"shapeType\":0\r\n },\r\n {\r\n \"panelId\":9,\r\n \"x\":374,\r\n \"y\":129,\r\n \"o\":180,\r\n \"shapeType\":0\r\n }\r\n ]\r\n }\r\n },\r\n \"rhythm\":{\r\n \"auxAvailable\":false,\r\n \"firmwareVersion\":\"2.4.3\",\r\n \"hardwareVersion\":\"2.0\",\r\n \"rhythmActive\":false,\r\n \"rhythmConnected\":true,\r\n \"rhythmId\":10,\r\n \"rhythmMode\":0,\r\n \"rhythmPos\":{\r\n \"x\":449.99521692839559,\r\n \"y\":86.60030339609753,\r\n \"o\":0.0\r\n }\r\n },\r\n \"schedules\":{\r\n\r\n },\r\n \"state\":{\r\n \"brightness\":{\r\n \"value\":29,\r\n \"max\":100,\r\n \"min\":0\r\n },\r\n \"colorMode\":\"effect\",\r\n \"ct\":{\r\n \"value\":3000,\r\n \"max\":6500,\r\n \"min\":1200\r\n },\r\n \"hue\":{\r\n \"value\":0,\r\n \"max\":360,\r\n \"min\":0\r\n },\r\n \"sat\":{\r\n \"value\":0,\r\n \"max\":100,\r\n \"min\":0\r\n }\r\n }\r\n}";
ControllerInfo controllerInfo = gson.fromJson(controllerInfoJSON, ControllerInfo.class);
assertThat(controllerInfo, is(notNullValue()));
final State state = controllerInfo.getState();
assertThat(state, is(notNullValue()));
assertThat(state.getOnOff(), is(OnOffType.OFF));
if (controllerInfo != null) {
final State state = controllerInfo.getState();
assertThat(state, is(notNullValue()));
assertThat(state.getOnOff(), is(OnOffType.OFF));
}
}
@Test
@@ -70,10 +76,12 @@ public class NanoleafControllerHandlerTest {
controllerInfoJSON = "{\r\n \"name\":\"Nanoleaf Light Panels 12:34:56\",\r\n \"serialNo\":\"S19082ABCDE\",\r\n \"manufacturer\":\"Nanoleaf\",\r\n \"firmwareVersion\":\"3.3.3\",\r\n \"hardwareVersion\":\"1.6-2\",\r\n \"model\":\"NL22\",\r\n \"cloudHash\":{\r\n\r\n },\r\n \"discovery\":{\r\n\r\n },\r\n \"effects\":{\r\n \"effectsList\":[\r\n \"Color Burst\",\r\n \"Fireworks\",\r\n \"Flames\",\r\n \"Forest\",\r\n \"Inner Peace\",\r\n \"Lightning\",\r\n \"Northern Lights\",\r\n \"Pulse Pop Beats\",\r\n \"Vibrant Sunrise\"\r\n ],\r\n \"select\":\"Flames\"\r\n },\r\n \"firmwareUpgrade\":{\r\n\r\n },\r\n \"panelLayout\":{\r\n \"globalOrientation\":{\r\n \"value\":0,\r\n \"max\":360,\r\n \"min\":0\r\n },\r\n \"layout\":{\r\n \"numPanels\":9,\r\n \"sideLength\":150,\r\n \"positionData\":[\r\n {\r\n \"panelId\":1,\r\n \"x\":299,\r\n \"y\":0,\r\n \"o\":300,\r\n \"shapeType\":0\r\n },\r\n {\r\n \"panelId\":2,\r\n \"x\":299,\r\n \"y\":86,\r\n \"o\":120,\r\n \"shapeType\":0\r\n },\r\n {\r\n \"panelId\":3,\r\n \"x\":224,\r\n \"y\":129,\r\n \"o\":60,\r\n \"shapeType\":0\r\n },\r\n {\r\n \"panelId\":4,\r\n \"x\":224,\r\n \"y\":216,\r\n \"o\":120,\r\n \"shapeType\":0\r\n },\r\n {\r\n \"panelId\":5,\r\n \"x\":149,\r\n \"y\":259,\r\n \"o\":60,\r\n \"shapeType\":0\r\n },\r\n {\r\n \"panelId\":6,\r\n \"x\":74,\r\n \"y\":216,\r\n \"o\":240,\r\n \"shapeType\":0\r\n },\r\n {\r\n \"panelId\":7,\r\n \"x\":0,\r\n \"y\":259,\r\n \"o\":60,\r\n \"shapeType\":0\r\n },\r\n {\r\n \"panelId\":8,\r\n \"x\":149,\r\n \"y\":346,\r\n \"o\":120,\r\n \"shapeType\":0\r\n },\r\n {\r\n \"panelId\":9,\r\n \"x\":374,\r\n \"y\":129,\r\n \"o\":180,\r\n \"shapeType\":0\r\n }\r\n ]\r\n }\r\n },\r\n \"rhythm\":{\r\n \"auxAvailable\":false,\r\n \"firmwareVersion\":\"2.4.3\",\r\n \"hardwareVersion\":\"2.0\",\r\n \"rhythmActive\":false,\r\n \"rhythmConnected\":true,\r\n \"rhythmId\":10,\r\n \"rhythmMode\":0,\r\n \"rhythmPos\":{\r\n \"x\":449.99521692839559,\r\n \"y\":86.60030339609753,\r\n \"o\":0.0\r\n }\r\n },\r\n \"schedules\":{\r\n\r\n },\r\n \"state\":{\r\n \"brightness\":{\r\n \"value\":29,\r\n \"max\":100,\r\n \"min\":0\r\n },\r\n \"colorMode\":\"effect\",\r\n \"ct\":{\r\n \"value\":3000,\r\n \"max\":6500,\r\n \"min\":1200\r\n },\r\n \"hue\":{\r\n \"value\":0,\r\n \"max\":360,\r\n \"min\":0\r\n },\r\n \"on\":{\r\n \"value\":false\r\n },\r\n \"sat\":{\r\n \"value\":0,\r\n \"max\":100,\r\n \"min\":0\r\n }\r\n }\r\n}";
ControllerInfo controllerInfo = gson.fromJson(controllerInfoJSON, ControllerInfo.class);
assertThat(controllerInfo, is(notNullValue()));
final State state = controllerInfo.getState();
assertThat(state, is(notNullValue()));
assertThat(state.getOnOff(), is(OnOffType.OFF));
if (controllerInfo != null) {
final State state = controllerInfo.getState();
assertThat(state, is(notNullValue()));
assertThat(state.getOnOff(), is(OnOffType.OFF));
}
}
}