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