From 99e4560a0e10ec0f4599c7fac7175930dd7156fa Mon Sep 17 00:00:00 2001 From: stefan-hoehn Date: Tue, 28 Sep 2021 09:07:12 +0200 Subject: [PATCH] [nanoleaf] Reimplement touch detection based on SSE, stabilize behavior, add swipe support (#11133) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [nanoleaf] reimplement touch detection based on sse, stabilize behavior * [nanoleaf] add swipe support * [nanoleaf] add / tested full shapes support Signed-off-by: Stefan Höhn Signed-off-by: Nick Waterton --- .../org.openhab.binding.nanoleaf/README.md | 134 ++++- .../doc/the-worm-small.png | Bin 0 -> 110849 bytes .../internal/NanoleafBindingConstants.java | 6 + .../internal/NanoleafHandlerFactory.java | 31 +- .../nanoleaf/internal/OpenAPIUtils.java | 99 ++-- .../command/NanoleafCommandExtension.java | 4 + .../NanoleafCommandDescriptionProvider.java | 18 +- .../NanoleafPanelsDiscoveryService.java | 22 +- .../handler/NanoleafControllerHandler.java | 557 ++++++++++-------- .../handler/NanoleafPanelHandler.java | 5 +- .../nanoleaf/internal/model/AuthToken.java | 5 +- .../nanoleaf/internal/model/Layout.java | 77 +-- .../internal/model/PositionDatum.java | 44 ++ .../nanoleaf/internal/model/State.java | 3 +- .../nanoleaf/internal/model/Write.java | 7 +- .../main/resources/OH-INF/config/config.xml | 2 +- .../resources/OH-INF/i18n/nanoleaf.properties | 2 + .../OH-INF/i18n/nanoleaf_de.properties | 2 + .../resources/OH-INF/thing/lightpanels.xml | 15 + .../binding/nanoleaf/internal/LayoutTest.java | 66 ++- ...PUUtilsTest.java => OpenAPIUtilsTest.java} | 2 +- .../binding/nanoleaf/internal/TouchTest.java | 14 +- .../NanoleafControllerHandlerTest.java | 32 +- 23 files changed, 728 insertions(+), 419 deletions(-) create mode 100755 bundles/org.openhab.binding.nanoleaf/doc/the-worm-small.png rename bundles/org.openhab.binding.nanoleaf/src/test/java/org/openhab/binding/nanoleaf/internal/{OpenAPUUtilsTest.java => OpenAPIUtilsTest.java} (97%) 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 0000000000000000000000000000000000000000..f0128e8f8fecd6a1d85a3893b4286a4b8e195869 GIT binary patch literal 110849 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 Z{|6U)g6wi_BzOP-002ovPDHLkV1hg(7a0Hm literal 0 HcmV?d00001 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)); + } } }