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