diff --git a/bundles/org.openhab.binding.nanoleaf/README.md b/bundles/org.openhab.binding.nanoleaf/README.md index 47ba4db643fb8..77858fdbc13f7 100644 --- a/bundles/org.openhab.binding.nanoleaf/README.md +++ b/bundles/org.openhab.binding.nanoleaf/README.md @@ -7,33 +7,33 @@ This binding integrates the [Nanoleaf Light Panels](https://nanoleaf.me/en/consu It enables you to authenticate, control, and obtain information of a Light Panel's device. The binding uses the [Nanoleaf OpenAPI](https://forum.nanoleaf.me/docs/openapi), which requires firmware version [1.5.0](https://helpdesk.nanoleaf.me/hc/en-us/articles/214006129-Light-Panels-Firmware-Release-Notes) or higher. -![Image](doc/LightPanels2_small.jpg) ![Image](doc/NanoCanvas_small.jpg) +![Image](doc/LightPanels2_small.jpg) ![Image](doc/the-worm-small.png) ![Image](doc/NanoCanvas_small.jpg) ## Supported Things Nanoleaf provides a bunch of devices of which some are connected to Wifi whereas other use the new Thread Technology. This binding only supports devices that are connected through Wifi. -Currently Nanoleaf's "Light Panels" and "Canvas" devices are supported. +Currently Nanoleaf's "Light Panels" and "Canvas/Shapes" devices are supported. The binding supports two thing types: controller and lightpanel. -The controller thing is the bridge for the individually attached panels/canvas and can be perceived as the Nanoleaf device at the wall as a whole (either called "light panels" or "canvas" by Nanoleaf). +The controller thing is the bridge for the individually attached panels/canvas and can be perceived as the Nanoleaf device at the wall as a whole (either called "light panels", "canvas" or "shapes" by Nanoleaf). With the controller thing you can control channels which affect all panels, e.g. selecting effects or setting the brightness. The lightpanel (singular) thing controls one of the individual panels/canvas that are connected to each other. Each individual panel has therefore its own id assigned to it. -You can set the **color** for each panel and in the case of a Nanoleaf canvas you can even detect single and double **touch events** related to an individual panel which opens a whole new world of controlling any other device within your openHAB environment. +You can set the **color** for each panel and in the case of a Nanoleaf Canvas or Shapes you can even detect single / double **touch events** related to an individual panel or **swipe events** on the whole device which opens a whole new world of controlling any other device within your openHAB environment. | Nanoleaf Name | Type | Description | supported | touch support | | ---------------------- | ---- | ---------------------------------------------------------- | --------- | ------------- | -| Light Panels | NL22 | Triangles 1st Generation | X | (-) | +| Light Panels | NL22 | Triangles 1st Generation | X | - | | Shapes Triangle | NL42 | Triangles 2nd Generation (rounded edges) | X | X | | Shapes Hexagon | NL42 | Hexagons | X | X | -| Shapes Mini Triangles | ?? | Mini Triangles | ? | ? | +| Shapes Mini Triangles | NL42 | Mini Triangles | x | X | | Canvas | NL29 | Squares | X | X | - x = Supported (x) = Supported but only tested by community (-) = unknown (no device available to test) + x = Supported (-) = unknown (no device available to test) ## Discovery @@ -72,11 +72,14 @@ In this case: Unfortunately it is not easy to find out which panel gets which id, and this becomes pretty important if you have lots of them and want to assign rules. -For canvas that use square panels, you can request the layout through a console command: +For canvas that use square panels, you can request the layout through a [console command](https://www.openhab.org/docs/administration/console.html): + +then issue the following command: ``` openhab:nanoleaf layout [] ``` + The `thingUID` is an optional parameter. If it is not provided, the command loops through all Nanoleaf controller things it can find and prints the layout for each of them. Compare the following output with the right picture at the beginning of the article @@ -117,23 +120,26 @@ This discovers all connected panels with their IDs. The controller bridge has the following channels: -| Channel | Item Type | Description | Read Only | -|---------------------|-----------|------------------------------------------------------------------------|-----------| -| color | Color | Color, power and brightness of all light panels | No | -| colorTemperature | Dimmer | Color temperature (in percent) of all light panels | No | -| colorTemperatureAbs | Number | Color temperature (in Kelvin, 1200 to 6500) of all light panels | No | -| colorMode | String | Color mode of the light panels | Yes | -| effect | String | Selected effect of the light panels | No | -| rhythmState | Switch | Connection state of the rhythm module | Yes | -| rhythmActive | Switch | Activity state of the rhythm module | Yes | -| rhythmMode | Number | Sound source for the rhythm module. 0=Microphone, 1=Aux cable | No | +| Channel | Item Type | Description | Read Only | +|---------------------|-----------|-----------------------------------------------------------------------------------------------------------|-----------| +| color | Color | Color, power and brightness of all light panels | No | +| colorTemperature | Dimmer | Color temperature (in percent) of all light panels | No | +| colorTemperatureAbs | Number | Color temperature (in Kelvin, 1200 to 6500) of all light panels | No | +| colorMode | String | Color mode of the light panels | Yes | +| effect | String | Selected effect of the light panels | No | +| rhythmState | Switch | Connection state of the rhythm module | Yes | +| rhythmActive | Switch | Activity state of the rhythm module | Yes | +| rhythmMode | Number | Sound source for the rhythm module. 0=Microphone, 1=Aux cable | No | +| swipe | Trigger | [Canvas / Shapes Only] Detects Swipes over the panel.LEFT, RIGHT, UP, DOWN events are supported. | YES | + + A lightpanel thing has the following channels: -| Channel | Type | Description | Read Only | -|---------------------|-----------|------------------------------------------------------------------------|-----------| -| color | Color | Color of the individual light panel | No | -| tap | Trigger | [Canvas Only] Sends events of gestures. Currently, these are SHORT_PRESSED and DOUBLE_PRESSED events. | Yes | +| Channel | Type | Description | Read Only | +|---------------------|-----------|----------------------------------------------------------------------------------------------------------|-----------| +| color | Color | Color of the individual light panel | No | +| tap | Trigger | [Canvas / Shapes Only] Sends events of gestures. SHORT_PRESSED and DOUBLE_PRESSED events are supported. | Yes | The color channels support full color control with hue, saturation and brightness values. For example, brightness of *all* panels at once can be controlled by defining a dimmer item for the color channel of the *controller thing*. @@ -150,15 +156,19 @@ The same applies to the color channel of an individual lightpanel. **Touch Support** Nanoleaf's Canvas introduces a whole new experience by supporting touch. This allows single and double taps on individual panels to be detected and processed via rules. -Note that even gestures like up, down, left, right are sent but can only be detected on the whole set of panels and not on an individual panel. These four gestures are not yet supported by the binding but may be added in a later release. -To detect single and double taps the panels have been extended to have two additional channels named singleTap and doubleTap which act like switches that are turned on as soon as a tap type is detected. -These switches then act as a pulse to further control anything else via rules. +Note that even gestures like up, down, left, right can be detected on the whole set of panels though not on an individual panel. +The four swipe gestures are supported by the binding. +See below for an example on how to use it. + +To detect single and double taps the panel provides a *tap* channel while the controller provides a *swipe* channel to detect swipes. -Keep in mind that the double tap is used as an already built-in functionality by default when you buy the nanoleaf: it switches all panels (hence the controller) to on or off like a light switch for all the panels at once. To circumvent that +Keep in mind that the double tap is used as an already built-in functionality by default when you buy the nanoleaf: it switches all panels (hence the controller) to on or off like a light switch for all the panels at once. +To circumvent that - Within the nanoleaf app go to the dashboard and choose your device. Enter the settings for that device by clicking the cog icon in the upper right corner. -- Enable "Touch Gesture" and assign the gestures you want to happen but set the double tap to unassigned. +- Enable "Touch Gesture" (the first radio button) and make sure that none of the gestures you use with openHAB is active. In general, it is recommended not to enable "touch sensitive gestures" (the second radio button). This prevents unexpected interference between openhHAB rules and Nanoleaf settings. + - To still have the possibility to switch on the whole canvas device with all its panels by double tapping a specific panel, you can easily write a rule that triggers on the tap channel of that panel and then sends an ON to the color channel of the controller. See the example below on Panel 1. More details can be found in the full example below. @@ -314,8 +324,78 @@ then sendCommand(NanoleafPower,OFF) } end + +// This is a complex rule controlling an item (e.g. a lamp) by swiping the nanoleaf but only if the swipe action has been triggered to become active. + +var brightnessMode = null +var oldEffect = null + +/* + +The idea behind that rule is to use one panel to switch on / off brightness control for a specific openHAB item. + + - In this case the panel with the id=36604 has been created as a thing. + - The controller color item is named SZNanoCanvas_Color + - The controller effect item that holds the last chosen effect is SZNanoCanvas_Effect + - Also that thing has channel to control the color of the panel + +We use that specific panel to toggle the brightness swipe mode on or off. +We indicate that mode by setting the canvas to red. When switching it +off we make sure we return the effect that was on before. +Only if the brightness swipe mode is ON we then use this to control the brightness of +another thing which in this case is a lamp. Every swipe changes the brightness by 10. +By extending it further this would also allow to select different items to control by +tapping different panels before. + +*/ + +rule "Enable swipe brightness mode" +when + Channel "nanoleaf:lightpanel:645E3A484FFF:31104:tap" triggered SHORT_PRESSED +then + if (brightnessMode == OFF || brightnessMode === null) { + brightnessMode = ON + oldEffect = SZNanoCanvas_Effect.state.toString + SZNanoCanvas_Color.sendCommand("0,100,100") + } else { + brightnessMode = OFF + sendCommand("SZNanoCanvas_Effect", oldEffect) + } +end + +rule "Swipe Nano to control brightness" +when + Channel "nanoleaf:controller:645E3A484FFF:swipe" triggered +then + // Note: you can even control a rollershutter instead of a light dimmer + var dimItem = MyLampDimmerItem + + // only process the swipe if brightness mode is active + if (brightnessMode == ON) { + var currentBrightness = dimItem.state as Number + switch (receivedEvent) { + case "LEFT": { + if (currentBrightness >= 10) { + currentBrightness = currentBrightness - 10 + } else { + currentBrightness = 0; + } + } + case "RIGHT": { + if (currentBrightness <= 90) { + currentBrightness = currentBrightness + 10 + } else { + currentBrightness = 100; + } + + } + } + sendCommand(dimItem, currentBrightness) + } +end ``` + ### nanoleaf.map ``` diff --git a/bundles/org.openhab.binding.nanoleaf/doc/the-worm-small.png b/bundles/org.openhab.binding.nanoleaf/doc/the-worm-small.png new file mode 100755 index 0000000000000..f0128e8f8fecd Binary files /dev/null and b/bundles/org.openhab.binding.nanoleaf/doc/the-worm-small.png differ diff --git a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/NanoleafBindingConstants.java b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/NanoleafBindingConstants.java index 6a4b08f7da80b..6ec8a777aa24a 100644 --- a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/NanoleafBindingConstants.java +++ b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/NanoleafBindingConstants.java @@ -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"; diff --git a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/NanoleafHandlerFactory.java b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/NanoleafHandlerFactory.java index f942a10f0d698..6c911a2ff3432 100644 --- a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/NanoleafHandlerFactory.java +++ b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/NanoleafHandlerFactory.java @@ -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 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 @@ @Component(configurationPid = "binding.nanoleaf", service = ThingHandlerFactory.class) public class NanoleafHandlerFactory extends BaseThingHandlerFactory { - public static final Set SUPPORTED_THING_TYPES_UIDS = Collections - .unmodifiableSet(Stream.of(THING_TYPE_LIGHT_PANEL, THING_TYPE_CONTROLLER).collect(Collectors.toSet())); + public static final Set 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; } } diff --git a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/OpenAPIUtils.java b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/OpenAPIUtils.java index af7658cb52c61..eb0433414e3bb 100644 --- a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/OpenAPIUtils.java +++ b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/OpenAPIUtils.java @@ -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 @@ */ @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 static URI getUri(NanoleafControllerConfig controllerConfig, String apiOp 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 static ContentResponse sendOpenAPIRequest(Request request) throws Nanolea 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 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 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) { - return false; - } - int[] currentVer = getFirmwareVersionNumbers(currentFirmwareVersion); + if (modelId != null && currentFirmwareVersion != null) { + int[] currentVer = getFirmwareVersionNumbers(currentFirmwareVersion); + int[] requiredVer = getFirmwareVersionNumbers("NL22".equals(modelId) ? "1.5.0" : "1.1.0"); - 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]) { + if (currentVer[i] > requiredVer[i]) { + return true; + } - for (int i = 0; i < currentVer.length; i++) { - if (currentVer[i] != requiredVer[i]) { - return currentVer[i] > requiredVer[i]; + return false; + } } + + return true; + } else { + return false; } - 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)) }; diff --git a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/command/NanoleafCommandExtension.java b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/command/NanoleafCommandExtension.java index d47e100729d19..a1e38836dbd44 100644 --- a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/command/NanoleafCommandExtension.java +++ b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/command/NanoleafCommandExtension.java @@ -59,6 +59,10 @@ public void execute(String[] args, Console console) { 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()); diff --git a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/commanddescription/NanoleafCommandDescriptionProvider.java b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/commanddescription/NanoleafCommandDescriptionProvider.java index 3d59ba3f53035..a7fcd7b6b0a83 100644 --- a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/commanddescription/NanoleafCommandDescriptionProvider.java +++ b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/commanddescription/NanoleafCommandDescriptionProvider.java @@ -15,7 +15,6 @@ 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 void setThingHandler(ThingHandler handler) { @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 effects = controllerInfo.getEffects().getEffectsList(); ChannelUID uid = effectChannelUID; if (effects != null && uid != null && uid.getThingUID().equals(bridge)) { - List<@NonNull CommandOption> commandOptions = effects.stream() // + List commandOptions = effects.stream() // .map(effect -> new CommandOption(effect, effect)) // .collect(Collectors.toList()); setCommandOptions(uid, commandOptions); diff --git a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/discovery/NanoleafPanelsDiscoveryService.java b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/discovery/NanoleafPanelsDiscoveryService.java index 73e4404718b9d..84ad726e88b39 100644 --- a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/discovery/NanoleafPanelsDiscoveryService.java +++ b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/discovery/NanoleafPanelsDiscoveryService.java @@ -33,6 +33,7 @@ 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 NanoleafPanelsDiscoveryService() { @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 void onControllerInfoFetched(ThingUID bridge, ControllerInfo controllerIn 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 @@ private void createResultsFromControllerInfo() { @Override public void setThingHandler(ThingHandler handler) { this.bridgeHandler = (NanoleafControllerHandler) handler; - this.bridgeHandler.registerControllerListener(this); + NanoleafControllerHandler localBridgeHandler = (NanoleafControllerHandler) handler; + + localBridgeHandler.registerControllerListener(this); } @Override diff --git a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/handler/NanoleafControllerHandler.java b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/handler/NanoleafControllerHandler.java index c36ef3bcf1f18..0ab54cc9a41ad 100644 --- a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/handler/NanoleafControllerHandler.java +++ b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/handler/NanoleafControllerHandler.java @@ -15,7 +15,6 @@ 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.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.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 controllerListeners = new CopyOnWriteArrayList<>(); - // Pairing, update and panel discovery jobs and touch event job + private @Nullable HttpClient httpClientSSETouchEvent; + private @Nullable Request sseTouchjobRequest; + private List controllerListeners = new CopyOnWriteArrayList(); + 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 void initialize() { setAddress(config.address); setPort(config.port); setRefreshIntervall(config.refreshInterval); - setAuthToken(config.authToken); - + String authToken = (config.authToken != null) ? config.authToken : ""; + setAuthToken(authToken); Map 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 void initialize() { } } - @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 NanoleafControllerConfig getControllerConfig() { } 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 @@ private synchronized void startUpdateJob() { } 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()); + 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()); + } + } + } - 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); + 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")); } - } else { - logger.error("starting TouchJob for Controller {} failed - missing token", this.getThing().getUID()); + 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 @@ private void runPairing() { } } - /** - * 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); - - 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); + 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(); + + 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"); - 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); + 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); + } - @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 -> { + 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()); + // 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()); + } } - } - }); + + }); + } }); } + /** + * Apply the swipe gesture to the controller + * + * @param gesture Only swipes are supported on the complete nanoleaf panels + */ + 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 @@ private void updateFromControllerInfo() throws NanoleafException { 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 @@ private void updateFromControllerInfo() throws NanoleafException { }); // 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 @@ private void sendStateCommand(String channel, Command command) throws NanoleafEx 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 @@ private void sendStateCommand(String channel, Command command) throws NanoleafEx } } } 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 @@ private void sendEffectCommand(Command command) throws NanoleafException { 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 @@ private void setRefreshIntervall(int refreshIntervall) { this.refreshIntervall = refreshIntervall; } - private @Nullable String getAuthToken() { + @Nullable + private String getAuthToken() { return authToken; } @@ -794,7 +866,8 @@ private void setAuthToken(@Nullable String authToken) { this.authToken = authToken; } - private @Nullable String getDeviceType() { + @Nullable + private String getDeviceType() { return deviceType; } diff --git a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/handler/NanoleafPanelHandler.java b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/handler/NanoleafPanelHandler.java index f25c54822ecd8..020f55f57b7b2 100644 --- a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/handler/NanoleafPanelHandler.java +++ b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/handler/NanoleafPanelHandler.java @@ -36,6 +36,7 @@ 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 diff --git a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/model/AuthToken.java b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/model/AuthToken.java index a92a6ea511041..ea9372c022800 100644 --- a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/model/AuthToken.java +++ b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/model/AuthToken.java @@ -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 @@ public class AuthToken { @SerializedName("auth_token") - private @Nullable String authToken; + private String authToken = ""; - public @Nullable String getAuthToken() { + public String getAuthToken() { return authToken; } diff --git a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/model/Layout.java b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/model/Layout.java index 2c3db6be1e568..8d88f91dd2c76 100644 --- a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/model/Layout.java +++ b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/model/Layout.java @@ -18,17 +18,21 @@ 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 positionData = null; @@ -40,14 +44,6 @@ public void setNumPanels(int numPanels) { this.numPanels = numPanels; } - public int getSideLength() { - return sideLength; - } - - public void setSideLength(int sideLength) { - this.sideLength = sideLength; - } - public @Nullable List getPositionData() { return positionData; } @@ -64,38 +60,46 @@ public void setPositionData(List positionData) { * @return a String containing the layout */ public String getLayoutView() { - if (positionData != null) { + List 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 = localPositionData.size(); - final int noofDefinedPanels = positionData.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 String getLayoutView() { 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 String getLayoutView() { 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 += " "; } diff --git a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/model/PositionDatum.java b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/model/PositionDatum.java index b5822a4cffa2d..e4e79f20ff492 100644 --- a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/model/PositionDatum.java +++ b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/model/PositionDatum.java @@ -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 panelSizes = new HashMap(); + + 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 void setPanelId(int panelId) { } 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 void setPosX(int x) { } 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 int getOrientation() { 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); + } } diff --git a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/model/State.java b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/model/State.java index 37e119ca8cdde..3cb4702e04fe2 100644 --- a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/model/State.java +++ b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/model/State.java @@ -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) { diff --git a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/model/Write.java b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/model/Write.java index 674bad7811630..ccf300619feaf 100644 --- a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/model/Write.java +++ b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/model/Write.java @@ -16,11 +16,13 @@ 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 = 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 void setPalette(List palette) { this.palette = palette; } - public String getColorType() { + public @Nullable String getColorType() { return colorType; } diff --git a/bundles/org.openhab.binding.nanoleaf/src/main/resources/OH-INF/config/config.xml b/bundles/org.openhab.binding.nanoleaf/src/main/resources/OH-INF/config/config.xml index 76ce1758656a2..28baf413edb9d 100644 --- a/bundles/org.openhab.binding.nanoleaf/src/main/resources/OH-INF/config/config.xml +++ b/bundles/org.openhab.binding.nanoleaf/src/main/resources/OH-INF/config/config.xml @@ -31,7 +31,7 @@ lightPanels - + diff --git a/bundles/org.openhab.binding.nanoleaf/src/main/resources/OH-INF/i18n/nanoleaf.properties b/bundles/org.openhab.binding.nanoleaf/src/main/resources/OH-INF/i18n/nanoleaf.properties index caf3424be8f77..bbd4ea80963b1 100644 --- a/bundles/org.openhab.binding.nanoleaf/src/main/resources/OH-INF/i18n/nanoleaf.properties +++ b/bundles/org.openhab.binding.nanoleaf/src/main/resources/OH-INF/i18n/nanoleaf.properties @@ -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. diff --git a/bundles/org.openhab.binding.nanoleaf/src/main/resources/OH-INF/i18n/nanoleaf_de.properties b/bundles/org.openhab.binding.nanoleaf/src/main/resources/OH-INF/i18n/nanoleaf_de.properties index d94531575837c..d798da8831c67 100644 --- a/bundles/org.openhab.binding.nanoleaf/src/main/resources/OH-INF/i18n/nanoleaf_de.properties +++ b/bundles/org.openhab.binding.nanoleaf/src/main/resources/OH-INF/i18n/nanoleaf_de.properties @@ -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. diff --git a/bundles/org.openhab.binding.nanoleaf/src/main/resources/OH-INF/thing/lightpanels.xml b/bundles/org.openhab.binding.nanoleaf/src/main/resources/OH-INF/thing/lightpanels.xml index 5aa026613d7e1..53d2488705866 100644 --- a/bundles/org.openhab.binding.nanoleaf/src/main/resources/OH-INF/thing/lightpanels.xml +++ b/bundles/org.openhab.binding.nanoleaf/src/main/resources/OH-INF/thing/lightpanels.xml @@ -17,6 +17,7 @@ + @@ -92,4 +93,18 @@ + + trigger + + @text/channel-type.nanoleaf.swipe.description + + + + + + + + + + diff --git a/bundles/org.openhab.binding.nanoleaf/src/test/java/org/openhab/binding/nanoleaf/internal/LayoutTest.java b/bundles/org.openhab.binding.nanoleaf/src/test/java/org/openhab/binding/nanoleaf/internal/LayoutTest.java index 069bb5635353e..d1f2e64dc7139 100644 --- a/bundles/org.openhab.binding.nanoleaf/src/test/java/org/openhab/binding/nanoleaf/internal/LayoutTest.java +++ b/bundles/org.openhab.binding.nanoleaf/src/test/java/org/openhab/binding/nanoleaf/internal/LayoutTest.java @@ -20,6 +20,7 @@ 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,21 +76,23 @@ public void setup() { 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(" 31413 9162 13276 \n" - + " \n" - + "55836 56093 48111 38724 17870 5164 64279 \n" - + " 8134 \n" - + " 58086 39755 \n" - + " \n" - + " 41451 \n"))); + 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" @@ -72,4 +103,19 @@ public void testTheInconsistentLayoutView() { + " \n" + " 41451 \n"))); } + + @Test + 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")))); + } } diff --git a/bundles/org.openhab.binding.nanoleaf/src/test/java/org/openhab/binding/nanoleaf/internal/OpenAPUUtilsTest.java b/bundles/org.openhab.binding.nanoleaf/src/test/java/org/openhab/binding/nanoleaf/internal/OpenAPIUtilsTest.java similarity index 97% rename from bundles/org.openhab.binding.nanoleaf/src/test/java/org/openhab/binding/nanoleaf/internal/OpenAPUUtilsTest.java rename to bundles/org.openhab.binding.nanoleaf/src/test/java/org/openhab/binding/nanoleaf/internal/OpenAPIUtilsTest.java index 6a11bd64eb322..f0ed4253a0054 100644 --- a/bundles/org.openhab.binding.nanoleaf/src/test/java/org/openhab/binding/nanoleaf/internal/OpenAPUUtilsTest.java +++ b/bundles/org.openhab.binding.nanoleaf/src/test/java/org/openhab/binding/nanoleaf/internal/OpenAPIUtilsTest.java @@ -25,7 +25,7 @@ */ @NonNullByDefault -public class OpenAPUUtilsTest { +public class OpenAPIUtilsTest { @Test public void testStateOn() { diff --git a/bundles/org.openhab.binding.nanoleaf/src/test/java/org/openhab/binding/nanoleaf/internal/TouchTest.java b/bundles/org.openhab.binding.nanoleaf/src/test/java/org/openhab/binding/nanoleaf/internal/TouchTest.java index 094987e4046c6..42d8dd1b4a938 100644 --- a/bundles/org.openhab.binding.nanoleaf/src/test/java/org/openhab/binding/nanoleaf/internal/TouchTest.java +++ b/bundles/org.openhab.binding.nanoleaf/src/test/java/org/openhab/binding/nanoleaf/internal/TouchTest.java @@ -16,6 +16,8 @@ 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 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)); } diff --git a/bundles/org.openhab.binding.nanoleaf/src/test/java/org/openhab/binding/nanoleaf/internal/handler/NanoleafControllerHandlerTest.java b/bundles/org.openhab.binding.nanoleaf/src/test/java/org/openhab/binding/nanoleaf/internal/handler/NanoleafControllerHandlerTest.java index 1028c2aa3d6d4..07237667e3b80 100644 --- a/bundles/org.openhab.binding.nanoleaf/src/test/java/org/openhab/binding/nanoleaf/internal/handler/NanoleafControllerHandlerTest.java +++ b/bundles/org.openhab.binding.nanoleaf/src/test/java/org/openhab/binding/nanoleaf/internal/handler/NanoleafControllerHandlerTest.java @@ -16,6 +16,7 @@ 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 void setup() { 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 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); + 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 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); + 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)); + } } }