[nanoleaf] Refactored code to use core features and more (#10101)

This is a bigger refactoring bringing these (breaking) changes:
- System channel types are used where applicable
- Obsolete channels (such as power) were removed
- Some channel types were marked "advanced"
- "Tap" channels were converted to a trigger channel type providing a "system button" behavior
- Layout can now be requested by a console command
- Command options for effect channel are dynamically provided
- Log level has been reduced where appropriate
- HTTP request timeouts were reduced
- handleRemoval now returns quickly as expected
- Fixed hanging thread / infinite loop when requesting layouts for non-square panels
- Various other smaller enhancements and fixes
- Documentation has been adapted accordingly

Signed-off-by: Kai Kreuzer <kai@openhab.org>
This commit is contained in:
Kai Kreuzer
2021-03-17 09:12:04 +01:00
committed by GitHub
parent c582dda1d5
commit 009208adee
15 changed files with 411 additions and 407 deletions

View File

@@ -11,35 +11,30 @@ The binding uses the [Nanoleaf OpenAPI](https://forum.nanoleaf.me/docs/openapi),
## 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 to Wifi.
Nanoleaf provides a bunch of devices of which some are connected to Wifi whereas other use the new Thread Technology. This binding only supports devices that are connected through Wifi.
Currently Nanoleaf's "Light Panels" and "Canvas" devices are supported.
Note that only specific types do support the touch functionality, so the binding needs to check these types.
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" or "canvas" 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 or turn it on (white) or off (black) 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 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.
| Nanoleaf Name | Type | Description | supported | touch support |
| ---------------------- | ---- | ---------------------------------------------------------- | --------- | ------------- |
| Light Panels | NL22 | Triangles 1st Generation | X | (-) |
| Shapes Triangle | NL42 | Triangles 2nd Generation (rounded edges) | X | X |
| Shapes Hexagon | NL42 | Triangles 2nd Generation (rounded edges) | (X) | (X) |
| Shapes Hexagon | NL42 | Hexagons | X | X |
| Shapes Mini Triangles | ?? | Mini Triangles | ? | ? |
| Canvas | NL29 | Squares | X | X |
x = Supported (x) = Supported but only tested by community (-) = unknown (no device available to test)
Note: In case of major changes of a binding (like adding more features to a thing) it becomes necessary to delete your things due to the things not being compatible anymore.
Don't worry too much though as they will be easily redetected and nothing really is lost.
Just make sure that you delete them and rediscover as described below.
## Discovery
**Adding the Controller as a Thing**
@@ -73,13 +68,16 @@ In this case:
- stop and then start openHAB
- Rediscover like described above
**Knowing which panel has which id**
### Panel Layout
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.
Don't worry as the binding comes with some helpful support in the background the canvas type (this is only provided for the canvas device because triangles can have weird layouts that are hard to express in a log output)
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.
- Set up a switch item with the channel panelLayout on the controller (see NanoRetrieveLayout below) and set the switch to true
- look out for something like "Panel layout and ids" in the openHAB logs. Below that you will see a panel layout similar to
For canvas that use square panels, you can request the layout through a console 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
@@ -93,9 +91,7 @@ Compare the following output with the right picture at the beginning of the arti
41451
```
Disclaimer: this works best with square devices and not necessarily well with triangles due to the more geometrically flexible layout.
## Thing Configuration
The controller thing has the following parameters:
@@ -106,7 +102,7 @@ The controller thing has the following parameters:
| port | Port number of the light panels contoller. Default is 16021 |
| authToken | The authentication token received from the controller after successful pairing. |
| refreshInterval | Interval in seconds to refresh the state of the light panels settings. Default is 60. |
| deviceType | (readOnly) defines the type: lightpanels (triangle) or canvas (square) |
| deviceType | Defines the type `lightpanels` (triangle) or `canvas` (square or hexagon) |
The lightpanel thing has the following parameters:
@@ -115,7 +111,7 @@ The lightpanel thing has the following parameters:
| id | ID assigned by the controller to the individual panel (e.g. 158) |
The IDs of the individual panels can be determined by starting another scan once the controller is configured and online.
This will add all connected panels with their IDs to the inbox.
This discovers all connected panels with their IDs.
## Channels
@@ -123,8 +119,7 @@ The controller bridge has the following channels:
| Channel | Item Type | Description | Read Only |
|---------------------|-----------|------------------------------------------------------------------------|-----------|
| power | Switch | Power state of the light panels | No |
| color | Color | Color of all light panels | No |
| color | Color | Color, power and brightness of all light panels | No |
| colorTemperature | Dimmer | Color temperature (in percent) of all light panels | No |
| colorTemperatureAbs | Number | Color temperature (in Kelvin, 1200 to 6500) of all light panels | No |
| colorMode | String | Color mode of the light panels | Yes |
@@ -132,48 +127,39 @@ The controller bridge has the following channels:
| 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 |
| panelLayout | Switch | Set to true will log out panel layout (returns to off automatically | No |
A lightpanel thing has the following channels:
| Channel | Item Type | Description | Read Only |
| Channel | Type | Description | Read Only |
|---------------------|-----------|------------------------------------------------------------------------|-----------|
| panelColor | Color | Color of the individual light panel | No |
| singleTap | Switch | [Canvas Only] Is set when the user taps that panel once (1 second pulse) | Yes |
| doubleTap | Switch | [Canvas Only] Is set when the user taps that panel twice (1 second pulse) | Yes |
| 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 |
**color and panelColor**
The color and panelColor channels support full color control with hue, saturation and brightness values.
The color channels support full color control with hue, saturation and brightness values.
For example, brightness of *all* panels at once can be controlled by defining a dimmer item for the color channel of the *controller thing*.
The same applies to the panelColor channel of an individual lightpanel thing.
What might not be obvious and even maybe confusing is the fact that brightness and color use the *same* channel but two different *itemTypes*. While the Color-itemtype controls the color, the Dimmer-itemtype controls the brightness on the same channel.
The same applies to the color channel of an individual lightpanel.
**Limitations assigning specific colors on individual panels:**
- Due to the way the API of the nanoleaf is designed, each time a color is assigned to a panel, it will be directly sent to that panel. The result is that if you send colors to several panels more or less at the same time, they will not be set at the same time but one after the other and rather appear like a sequence but as a one shot.
- Another important limitation is that individual panels cannot be set while a dynamic effect is running on the panel which means that the following happens
- As soon as you set an individual panel a so called "static effect" is created which replaces the chosen dynamic effect. You can even see that in the nanoleaf app that shows that a static effect is now running.
- Unfortunately, at least at the moment, the colors of the current state cannot be retrieved due to the high frequency of color changes that cannot be read quickly enough from the canvas, so all panels go to OFF
- The the first panelColor command is applied to that panel (and of course then all subsequent commands)
- The fact that it is called a static effect does not mean that you cannot create animations. The Rainbow rule below shows a good example for the whole canvas. Just replace the controller item with a panel item and you will get the rainbow effect with an individual panel.
- Another important limitation is that individual panels cannot be set while a dynamic effect is running on the panel which means that as soon as you set an individual panel the "static effect" is set, which disables the chosen dynamic effect. The nanoleaf app shows that a static effect is now running, too.
- The colors of the current state cannot be retrieved due to the high frequency of color changes that cannot be read quickly enough from the canvas, so all panels go to OFF
- The first panelColor command is applied to that panel (and of course then all subsequent commands)
- The fact that it is called a static effect does not mean that you cannot create animations. The Rainbow rule below shows a good example for the whole canvas. Just replace the controller item with a panel item and you will get the rainbow effect with an individual panel.
**Touch Support**
Nanoleaf's Canvas introduces a whole new experience by adding touch support to it. This allows single and double taps on individual panels to be detected and then processed via rules to further control any other device!
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 panel's 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.
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.
If a panel is tapped the switch is set to ON and automatically reset to OFF after 1 second (this may be configured in the future) to simulate a pulse. A rule can easily detect the transition from OFF to ON and later detect another tap as it is automatically reset by the binding. See the example below on Panel 2.
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.
- 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 double tap channel of that panel and then toggles the Power Channel of the controller. See the example below on Panel 1.
- To still have the possibility to switch on the whole canvas device with all its panels by double tapping a specific panel, you can easily write a rule that triggers on the tap channel of that panel and then sends an ON to the color channel of the controller. See the example below on Panel 1.
More details can be found in the full example below.
@@ -190,7 +176,7 @@ Bridge nanoleaf:controller:MyLightPanels @ "mylocation" [ address="192.168.1.100
}
```
If you define your device statically in the thing file, autodiscovery of the same thing is suppressed by using
If you define your device statically in the thing file, auto-discovery of the same thing is suppressed by using
* the [address="..." ] of the controller
* and the [id=123] of the lightpanel
@@ -208,13 +194,13 @@ e.g. via command line `curl --location --request POST 'http://<address>:16021/ap
### nanoleaf.items
Note: If you did autodiscover your things and items:
Note: If you auto-discovered your things and items:
- A controller item looks like nanoleaf:controller:F0ED4F9351AF:power where F0ED4F9351AF is the id of the controller that has been automatically assigned by the binding.
- A panel item looks like nanoleaf:lightpanel:F0ED4F9351AF:39755:singleTap where 39755 is the id of the panel that has been automatically assigned by the binding.
```
Switch NanoleafPower "Nanoleaf" { channel="nanoleaf:controller:MyLightPanels:power" }
Switch NanoleafPower "Nanoleaf" { channel="nanoleaf:controller:MyLightPanels:color" }
Color NanoleafColor "Color" { channel="nanoleaf:controller:MyLightPanels:color" }
Dimmer NanoleafBrightness "Brightness [%.0f]" { channel="nanoleaf:controller:MyLightPanels:color" }
String NanoleafHue "Hue [%s]"
@@ -226,15 +212,11 @@ String NanoleafEffect "Effect" { channel="nanoleaf:controller:MyLightPanels:effe
Switch NanoleafRhythmState "Rhythm connected [MAP(nanoleaf.map):%s]" { channel="nanoleaf:controller:MyLightPanels:rhythmState" }
Switch NanoleafRhythmActive "Rhythm active [MAP(nanoleaf.map):%s]" { channel="nanoleaf:controller:MyLightPanels:rhythmActive" }
Number NanoleafRhythmSource "Rhythm source [%s]" { channel="nanoleaf:controller:MyLightPanels:rhythmMode" }
Switch NanoRetrieveLayout "Nano Layout" { channel="nanoleaf:controller:D81E7A7E424E:panelLayout" }
// note that the next to items use the exact same channel but the two different types Color and Dimmer to control different parameters
Color Panel1Color "Panel 1" { channel="nanoleaf:lightpanel:MyLightPanels:135:panelColor" }
Dimmer Panel1Brightness "Panel 1" { channel="nanoleaf:lightpanel:MyLightPanels:135:panelColor" }
Switch Panel1DoubleTap "Toggle device on and off" { channel="nanoleaf:lightpanel:MyLightPanels:135:doubleTap" }
Switch Panel2Color "Panel 2" { channel="nanoleaf:lightpanel:MyLightPanels:158:panelColor" }
Switch Panel2SingleTap "Panel 2 Single Tap" { channel="nanoleaf:lightpanel:MyLightPanels:158:singleTap" }
Switch Panel2DoubleTap "Panel 2 Double Tap" { channel="nanoleaf:lightpanel:MyLightPanels:158:doubleTap" }
Color PanelColor "Panel 1" { channel="nanoleaf:lightpanel:MyLightPanels:135:color" }
Dimmer Panel1Brightness "Panel 1" { channel="nanoleaf:lightpanel:MyLightPanels:135:color" }
Switch Panel2Color "Panel 2" { channel="nanoleaf:lightpanel:MyLightPanels:158:color" }
Switch NanoleafRainbowScene "Show Rainbow Scene"
```
@@ -252,11 +234,10 @@ sitemap nanoleaf label="Nanoleaf"
Slider item=NanoleafColorTemp
Setpoint item=NanoleafColorTempAbs step=100 minValue=1200 maxValue=6500
Text item=NanoleafColorMode
Selection item=NanoleafEffect mappings=["Color Burst"="Color Burst", "Fireworks" = "Fireworks", "Flames" = "Flames", "Forest" = "Forest", "Inner Peace" = "Inner Peace", "Meteor Shower" = "Meteor Shower", "Nemo" = "Nemo", "Northern Lights" = "Northern Lights", "Paint Splatter" = "Paint Splatter", "Pulse Pop Beats" = "Pulse Pop Beats", "Rhythmic Northern Lights" = "Rhythmic Northern Lights", "Ripple" = "Ripple", "Romantic" = "Romantic", "Snowfall" = "Snowfall", "Sound Bar" = "Sound Bar", "Streaking Notes" = "Streaking Notes", "moonlight" = "Moonlight", "*Static*" = "Color (single panels)", "*Dynamic*" = "Color (all panels)" ]
Selection item=NanoleafEffect
Text item=NanoleafRhythmState
Text item=NanoleafRhythmActive
Selection item=NanoleafRhythmSource mappings=[0="Microphone", 1="Aux"]
Switch item=NanoRetrieveLayout
}
Frame label="Panels" {
@@ -271,9 +252,6 @@ sitemap nanoleaf label="Nanoleaf"
}
```
Note: The mappings to effects in the selection item are specific for each Nanoleaf installation and should be adapted accordingly.
Only the effects "\*Static\*" and "\*Dynamic\*" are predefined by the controller and should always be present in the mappings.
### nanoleaf.rules
```
@@ -313,8 +291,7 @@ end
rule "Nanoleaf canvas touch detection Panel 2"
when
Item Panel2SingleTap changed from NULL to ON or
Item Panel2SingleTap changed from OFF to ON
Channel "nanoleaf:lightpanel:MyLightPanels:158:tap" triggered SHORT_PRESS
then
logInfo("CanvasTouch", "Nanoleaf Canvas Panel 2 was touched once")
@@ -327,8 +304,7 @@ end
rule "Nanoleaf double tap toggles power of device"
when
Item Panel1DoubleTap changed from NULL to ON or
Item Panel1DoubleTap changed from OFF to ON
Channel "nanoleaf:lightpanel:MyLightPanels:135:tap" triggered DOUBLE_PRESS
then
logInfo("CanvasTouch", "Nanoleaf Canvas Panel 1 was touched twice. Toggle Power of whole canvas.")

View File

@@ -27,7 +27,7 @@ import org.openhab.core.thing.ThingTypeUID;
@NonNullByDefault
public class NanoleafBindingConstants {
private static final String BINDING_ID = "nanoleaf";
public static final String BINDING_ID = "nanoleaf";
// List of all Thing Type UIDs
public static final ThingTypeUID THING_TYPE_CONTROLLER = new ThingTypeUID(BINDING_ID, "controller");
@@ -44,7 +44,6 @@ public class NanoleafBindingConstants {
public static final String CONFIG_PANEL_ID = "id";
// List of controller channels
public static final String CHANNEL_POWER = "power";
public static final String CHANNEL_COLOR = "color";
public static final String CHANNEL_COLOR_TEMPERATURE = "colorTemperature";
public static final String CHANNEL_COLOR_TEMPERATURE_ABS = "colorTemperatureAbs";
@@ -53,12 +52,10 @@ 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_PANEL_LAYOUT = "panelLayout";
// List of light panel channels
public static final String CHANNEL_PANEL_COLOR = "panelColor";
public static final String CHANNEL_PANEL_SINGLE_TAP = "singleTap";
public static final String CHANNEL_PANEL_DOUBLE_TAP = "doubleTap";
public static final String CHANNEL_PANEL_COLOR = "color";
public static final String CHANNEL_PANEL_TAP = "tap";
// Nanoleaf OpenAPI URLs
public static final String API_V1_BASE_URL = "/api/v1";

View File

@@ -15,9 +15,6 @@ package org.openhab.binding.nanoleaf.internal;
import static org.openhab.binding.nanoleaf.internal.NanoleafBindingConstants.*;
import java.util.Collections;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@@ -25,19 +22,15 @@ 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.discovery.NanoleafPanelsDiscoveryService;
import org.openhab.binding.nanoleaf.internal.handler.NanoleafControllerHandler;
import org.openhab.binding.nanoleaf.internal.handler.NanoleafPanelHandler;
import org.openhab.core.config.discovery.DiscoveryService;
import org.openhab.core.io.net.http.HttpClientFactory;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerFactory;
import org.osgi.framework.ServiceRegistration;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
@@ -49,6 +42,7 @@ import org.slf4j.LoggerFactory;
* and panel (thing) handlers.
*
* @author Martin Raepple - Initial contribution
* @author Kai Kreuzer - made discovery a handler service
*/
@NonNullByDefault
@Component(configurationPid = "binding.nanoleaf", service = ThingHandlerFactory.class)
@@ -58,7 +52,6 @@ public class NanoleafHandlerFactory extends BaseThingHandlerFactory {
.unmodifiableSet(Stream.of(THING_TYPE_LIGHT_PANEL, THING_TYPE_CONTROLLER).collect(Collectors.toSet()));
private final Logger logger = LoggerFactory.getLogger(NanoleafHandlerFactory.class);
private final Map<ThingUID, ServiceRegistration<?>> discoveryServiceRegs = new HashMap<>();
private final HttpClient httpClient;
@Activate
@@ -77,7 +70,6 @@ public class NanoleafHandlerFactory extends BaseThingHandlerFactory {
if (THING_TYPE_CONTROLLER.equals(thingTypeUID)) {
NanoleafControllerHandler handler = new NanoleafControllerHandler((Bridge) thing, httpClient);
registerDiscoveryService(handler);
logger.debug("Nanoleaf controller handler created.");
return handler;
} else if (THING_TYPE_LIGHT_PANEL.equals(thingTypeUID)) {
@@ -87,30 +79,4 @@ public class NanoleafHandlerFactory extends BaseThingHandlerFactory {
}
return null;
}
@Override
protected void removeHandler(ThingHandler thingHandler) {
if (thingHandler instanceof NanoleafControllerHandler) {
unregisterDiscoveryService(thingHandler.getThing());
logger.debug("Nanoleaf controller handler removed.");
}
}
private synchronized void registerDiscoveryService(NanoleafControllerHandler bridgeHandler) {
NanoleafPanelsDiscoveryService discoveryService = new NanoleafPanelsDiscoveryService(bridgeHandler);
discoveryServiceRegs.put(bridgeHandler.getThing().getUID(),
bundleContext.registerService(DiscoveryService.class.getName(), discoveryService, new Hashtable<>()));
logger.debug("Discovery service for panels registered.");
}
@SuppressWarnings("null")
private synchronized void unregisterDiscoveryService(Thing thing) {
@Nullable
ServiceRegistration<?> serviceReg = discoveryServiceRegs.remove(thing.getUID());
// would require null check but "if (response!=null)" throws warning on comoile time :´-(
if (serviceReg != null) {
serviceReg.unregister();
}
logger.debug("Discovery service for panels removed.");
}
}

View File

@@ -20,6 +20,7 @@ import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.Iterator;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -57,7 +58,7 @@ public class OpenAPIUtils {
LOGGER.trace("RequestBuilder: Sending Request {}:{} {} ", requestURI.getHost(), requestURI.getPort(),
requestURI.getPath());
return httpClient.newRequest(requestURI).method(method);
return httpClient.newRequest(requestURI).method(method).timeout(10, TimeUnit.SECONDS);
}
public static URI getUri(NanoleafControllerConfig controllerConfig, String apiOperation, @Nullable String query)

View File

@@ -0,0 +1,103 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.nanoleaf.internal.command;
import java.util.Arrays;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.nanoleaf.internal.NanoleafBindingConstants;
import org.openhab.binding.nanoleaf.internal.handler.NanoleafControllerHandler;
import org.openhab.core.io.console.Console;
import org.openhab.core.io.console.extensions.AbstractConsoleCommandExtension;
import org.openhab.core.io.console.extensions.ConsoleCommandExtension;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingRegistry;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.thing.binding.ThingHandler;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
/**
* Console commands for interacting with Nanoleaf integration
*
* @author Kai Kreuzer - Initial contribution
*/
@NonNullByDefault
@Component(service = ConsoleCommandExtension.class)
public class NanoleafCommandExtension extends AbstractConsoleCommandExtension {
private static final String CMD_LAYOUT = "layout";
private final ThingRegistry thingRegistry;
@Activate
public NanoleafCommandExtension(@Reference ThingRegistry thingRegistry) {
super("nanoleaf", "Interact with the Nanoleaf integration.");
this.thingRegistry = thingRegistry;
}
@Override
public void execute(String[] args, Console console) {
if (args.length > 0) {
String subCommand = args[0];
switch (subCommand) {
case CMD_LAYOUT:
if (args.length == 1) {
thingRegistry.getAll().forEach(thing -> {
if (thing.getUID().getBindingId().equals(NanoleafBindingConstants.BINDING_ID)) {
ThingHandler handler = thing.getHandler();
if (handler instanceof NanoleafControllerHandler) {
NanoleafControllerHandler nanoleafControllerHandler = (NanoleafControllerHandler) handler;
String layout = nanoleafControllerHandler.getLayout();
console.println("Layout of Nanoleaf controller '" + thing.getUID().getAsString()
+ "' with label '" + thing.getLabel() + "':" + System.lineSeparator());
console.println(layout);
console.println(System.lineSeparator());
}
}
});
} else if (args.length == 2) {
String uid = args[1];
Thing thing = thingRegistry.get(new ThingUID(uid));
if (thing != null) {
ThingHandler handler = thing.getHandler();
if (handler instanceof NanoleafControllerHandler) {
NanoleafControllerHandler nanoleafControllerHandler = (NanoleafControllerHandler) handler;
String layout = nanoleafControllerHandler.getLayout();
console.println(layout);
} else {
console.println("Thing with UID '" + uid.toString()
+ "' is not an initialized Nanoleaf controller.");
}
} else {
console.println("Thing with UID '" + uid.toString() + "' does not exist.");
}
} else {
printUsage(console);
}
break;
default:
console.println("Unknown command '" + subCommand + "'");
printUsage(console);
break;
}
}
}
@Override
public List<String> getUsages() {
return Arrays.asList(buildCommandUsage(CMD_LAYOUT + " <thingUID>", "Prints the panel layout on the console."));
}
}

View File

@@ -0,0 +1,80 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
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;
import org.openhab.binding.nanoleaf.internal.NanoleafControllerListener;
import org.openhab.binding.nanoleaf.internal.handler.NanoleafControllerHandler;
import org.openhab.binding.nanoleaf.internal.model.ControllerInfo;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.thing.binding.BaseDynamicCommandDescriptionProvider;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerService;
import org.openhab.core.thing.type.DynamicCommandDescriptionProvider;
import org.openhab.core.types.CommandOption;
import org.osgi.service.component.annotations.Component;
/**
* This class provides the available effects as dynamic options as they are read from the Nanoleaf controller.
*
* @author Kai Kreuzer - Initial contribution
*
*/
@NonNullByDefault
@Component(service = { DynamicCommandDescriptionProvider.class })
public class NanoleafCommandDescriptionProvider extends BaseDynamicCommandDescriptionProvider
implements NanoleafControllerListener, ThingHandlerService {
private @Nullable ChannelUID effectChannelUID;
private @Nullable NanoleafControllerHandler bridgeHandler;
@Override
public void setThingHandler(ThingHandler handler) {
this.bridgeHandler = (NanoleafControllerHandler) handler;
bridgeHandler.registerControllerListener(this);
effectChannelUID = new ChannelUID(handler.getThing().getUID(), NanoleafBindingConstants.CHANNEL_EFFECT);
}
@Override
public @Nullable ThingHandler getThingHandler() {
return bridgeHandler;
}
@Override
public void deactivate() {
if (bridgeHandler != null) {
bridgeHandler.unregisterControllerListener(this);
}
super.deactivate();
}
@Override
public void onControllerInfoFetched(@NonNull ThingUID bridge, @NonNull ControllerInfo controllerInfo) {
List<@NonNull String> effects = controllerInfo.getEffects().getEffectsList();
ChannelUID uid = effectChannelUID;
if (effects != null && uid != null && uid.getThingUID().equals(bridge)) {
List<@NonNull CommandOption> commandOptions = effects.stream() //
.map(effect -> new CommandOption(effect, effect)) //
.collect(Collectors.toList());
setCommandOptions(uid, commandOptions);
}
}
}

View File

@@ -79,10 +79,10 @@ public class NanoleafMDNSDiscoveryParticipant implements MDNSDiscoveryParticipan
logger.trace("Discovered nanoleaf host: {} port: {} firmWare: {} modelId: {} qualifiedName: {}", host, port,
firmwareVersion, modelId, qualifiedName);
logger.debug("Adding Nanoleaf controller {} with FW version {} found at {} {} to inbox", qualifiedName,
logger.debug("Adding Nanoleaf controller {} with FW version {} found at {}:{} to inbox", qualifiedName,
firmwareVersion, host, port);
if (!OpenAPIUtils.checkRequiredFirmware(service.getPropertyString("md"), firmwareVersion)) {
logger.warn("Nanoleaf controller firmware is too old. Must be {} or higher",
logger.debug("Nanoleaf controller firmware is too old. Must be {} or higher",
MODEL_ID_LIGHTPANELS.equals(modelId) ? API_MIN_FW_VER_LIGHTPANELS : API_MIN_FW_VER_CANVAS);
}

View File

@@ -33,6 +33,8 @@ 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.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -41,36 +43,37 @@ import org.slf4j.LoggerFactory;
* panels connected to the controller.
*
* @author Martin Raepple - Initial contribution
* @author Kai Kreuzer - Made it a ThingHandlerService
*/
@NonNullByDefault
public class NanoleafPanelsDiscoveryService extends AbstractDiscoveryService implements NanoleafControllerListener {
public class NanoleafPanelsDiscoveryService extends AbstractDiscoveryService
implements NanoleafControllerListener, ThingHandlerService {
private static final int SEARCH_TIMEOUT_SECONDS = 60;
private final Logger logger = LoggerFactory.getLogger(NanoleafPanelsDiscoveryService.class);
private final NanoleafControllerHandler bridgeHandler;
private @Nullable NanoleafControllerHandler bridgeHandler;
private @Nullable ControllerInfo controllerInfo;
/**
* Constructs a new {@link NanoleafPanelsDiscoveryService} attached to the given bridge handler.
*
* @param nanoleafControllerHandler The bridge handler this discovery service is attached to
* Constructs a new {@link NanoleafPanelsDiscoveryService}.
*/
public NanoleafPanelsDiscoveryService(NanoleafControllerHandler nanoleafControllerHandler) {
public NanoleafPanelsDiscoveryService() {
super(NanoleafHandlerFactory.SUPPORTED_THING_TYPES_UIDS, SEARCH_TIMEOUT_SECONDS, false);
this.bridgeHandler = nanoleafControllerHandler;
}
@Override
public void deactivate() {
if (bridgeHandler != null) {
bridgeHandler.unregisterControllerListener(this);
}
super.deactivate();
}
@Override
protected void startScan() {
logger.debug("Starting Nanoleaf panel discovery");
bridgeHandler.registerControllerListener(this);
}
@Override
protected synchronized void stopScan() {
logger.debug("Stopping Nanoleaf panel discovery");
super.stopScan();
bridgeHandler.unregisterControllerListener(this);
createResultsFromControllerInfo();
}
/**
@@ -81,38 +84,60 @@ public class NanoleafPanelsDiscoveryService extends AbstractDiscoveryService imp
*/
@Override
public void onControllerInfoFetched(ThingUID bridge, ControllerInfo controllerInfo) {
logger.debug("Discover panels connected to controller with id {}", bridge.getAsString());
final PanelLayout panelLayout = controllerInfo.getPanelLayout();
@Nullable
Layout layout = panelLayout.getLayout();
if (layout != null && layout.getNumPanels() > 0) {
@Nullable
final List<PositionDatum> positionData = layout.getPositionData();
if (positionData != null) {
Iterator<PositionDatum> iterator = positionData.iterator();
while (iterator.hasNext()) {
@Nullable
PositionDatum panel = iterator.next();
ThingUID newPanelThingUID = new ThingUID(NanoleafBindingConstants.THING_TYPE_LIGHT_PANEL, bridge,
Integer.toString(panel.getPanelId()));
final Map<String, Object> properties = new HashMap<>(1);
properties.put(CONFIG_PANEL_ID, panel.getPanelId());
DiscoveryResult newPanel = DiscoveryResultBuilder.create(newPanelThingUID).withBridge(bridge)
.withProperties(properties).withLabel("Light Panel " + panel.getPanelId())
.withRepresentationProperty(CONFIG_PANEL_ID).build();
logger.debug("Adding panel with id {} to inbox", panel.getPanelId());
thingDiscovered(newPanel);
}
} else {
logger.debug("Couldn't add panels to inbox as layout position data was null");
}
this.controllerInfo = controllerInfo;
}
private void createResultsFromControllerInfo() {
ThingUID bridgeUID;
if (bridgeHandler != null) {
bridgeUID = bridgeHandler.getThing().getUID();
} else {
logger.info("No panels found or connected to controller");
return;
}
if (controllerInfo != null) {
final PanelLayout panelLayout = controllerInfo.getPanelLayout();
@Nullable
Layout layout = panelLayout.getLayout();
if (layout != null && layout.getNumPanels() > 0) {
@Nullable
final List<PositionDatum> positionData = layout.getPositionData();
if (positionData != null) {
Iterator<PositionDatum> iterator = positionData.iterator();
while (iterator.hasNext()) {
@Nullable
PositionDatum panel = iterator.next();
ThingUID newPanelThingUID = new ThingUID(NanoleafBindingConstants.THING_TYPE_LIGHT_PANEL,
bridgeUID, Integer.toString(panel.getPanelId()));
final Map<String, Object> properties = new HashMap<>(1);
properties.put(CONFIG_PANEL_ID, panel.getPanelId());
DiscoveryResult newPanel = DiscoveryResultBuilder.create(newPanelThingUID).withBridge(bridgeUID)
.withProperties(properties).withLabel("Light Panel " + panel.getPanelId())
.withRepresentationProperty(CONFIG_PANEL_ID).build();
logger.debug("Adding panel with id {} to inbox", panel.getPanelId());
thingDiscovered(newPanel);
}
} else {
logger.debug("Couldn't add panels to inbox as layout position data was null");
}
} else {
logger.debug("No panels found or connected to controller");
}
}
}
@Override
public void setThingHandler(ThingHandler handler) {
this.bridgeHandler = (NanoleafControllerHandler) handler;
this.bridgeHandler.registerControllerListener(this);
}
@Override
public @Nullable ThingHandler getThingHandler() {
return bridgeHandler;
}
}

View File

@@ -17,7 +17,11 @@ import static org.openhab.binding.nanoleaf.internal.NanoleafBindingConstants.*;
import java.net.URI;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Scanner;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ScheduledFuture;
@@ -36,10 +40,11 @@ import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.HttpStatus;
import org.openhab.binding.nanoleaf.internal.NanoleafControllerListener;
import org.openhab.binding.nanoleaf.internal.NanoleafException;
import org.openhab.binding.nanoleaf.internal.NanoleafInterruptedException;
import org.openhab.binding.nanoleaf.internal.NanoleafUnauthorizedException;
import org.openhab.binding.nanoleaf.internal.OpenAPIUtils;
import org.openhab.binding.nanoleaf.internal.commanddescription.NanoleafCommandDescriptionProvider;
import org.openhab.binding.nanoleaf.internal.config.NanoleafControllerConfig;
import org.openhab.binding.nanoleaf.internal.discovery.NanoleafPanelsDiscoveryService;
import org.openhab.binding.nanoleaf.internal.model.AuthToken;
import org.openhab.binding.nanoleaf.internal.model.BooleanState;
import org.openhab.binding.nanoleaf.internal.model.Brightness;
@@ -67,6 +72,7 @@ import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.binding.BaseBridgeHandler;
import org.openhab.core.thing.binding.ThingHandlerService;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.slf4j.Logger;
@@ -81,15 +87,13 @@ import com.google.gson.JsonSyntaxException;
*
* @author Martin Raepple - Initial contribution
* @author Stefan Höhn - Canvas Touch Support
* @author Kai Kreuzer - refactoring, bug fixing and code clean up
*/
@NonNullByDefault
public class NanoleafControllerHandler extends BaseBridgeHandler {
// Pairing interval in seconds
private static final int PAIRING_INTERVAL = 25;
// Panel discovery interval in seconds
private static final int PANEL_DISCOVERY_INTERVAL = 30;
private static final int PAIRING_INTERVAL = 10;
private final Logger logger = LoggerFactory.getLogger(NanoleafControllerHandler.class);
private HttpClient httpClient;
@@ -98,7 +102,6 @@ public class NanoleafControllerHandler extends BaseBridgeHandler {
// Pairing, update and panel discovery jobs and touch event job
private @NonNullByDefault({}) ScheduledFuture<?> pairingJob;
private @NonNullByDefault({}) ScheduledFuture<?> updateJob;
private @NonNullByDefault({}) ScheduledFuture<?> panelDiscoveryJob;
private @NonNullByDefault({}) ScheduledFuture<?> touchJob;
// JSON parser for API responses
@@ -120,7 +123,7 @@ public class NanoleafControllerHandler extends BaseBridgeHandler {
@Override
public void initialize() {
logger.debug("Initializing the controller (bridge)");
updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.BRIDGE_UNINITIALIZED);
updateStatus(ThingStatus.UNKNOWN);
NanoleafControllerConfig config = getConfigAs(NanoleafControllerConfig.class);
setAddress(config.address);
setPort(config.port);
@@ -157,13 +160,9 @@ public class NanoleafControllerHandler extends BaseBridgeHandler {
"@text/error.nanoleaf.controller.noToken");
startPairingJob();
stopUpdateJob();
stopPanelDiscoveryJob();
} else {
logger.debug("Controller is online. Stop pairing job, start update & panel discovery jobs");
updateStatus(ThingStatus.ONLINE);
stopPairingJob();
startUpdateJob();
startPanelDiscoveryJob();
startTouchJob();
}
} catch (IllegalArgumentException iae) {
@@ -186,11 +185,9 @@ public class NanoleafControllerHandler extends BaseBridgeHandler {
updateFromControllerInfo();
} else {
switch (channelUID.getId()) {
case CHANNEL_POWER:
case CHANNEL_COLOR:
case CHANNEL_COLOR_TEMPERATURE:
case CHANNEL_COLOR_TEMPERATURE_ABS:
case CHANNEL_PANEL_LAYOUT:
sendStateCommand(channelUID.getId(), command);
break;
case CHANNEL_EFFECT:
@@ -218,25 +215,28 @@ public class NanoleafControllerHandler extends BaseBridgeHandler {
@Override
public void handleRemoval() {
// delete token for openHAB
ContentResponse deleteTokenResponse;
try {
Request deleteTokenRequest = OpenAPIUtils.requestBuilder(httpClient, getControllerConfig(), API_DELETE_USER,
HttpMethod.DELETE);
deleteTokenResponse = OpenAPIUtils.sendOpenAPIRequest(deleteTokenRequest);
if (deleteTokenResponse.getStatus() != HttpStatus.NO_CONTENT_204) {
logger.warn("Failed to delete token for openHAB. Response code is {}", deleteTokenResponse.getStatus());
return;
scheduler.execute(() -> {
// delete token for openHAB
ContentResponse deleteTokenResponse;
try {
Request deleteTokenRequest = OpenAPIUtils.requestBuilder(httpClient, getControllerConfig(),
API_DELETE_USER, HttpMethod.DELETE);
deleteTokenResponse = OpenAPIUtils.sendOpenAPIRequest(deleteTokenRequest);
if (deleteTokenResponse.getStatus() != HttpStatus.NO_CONTENT_204) {
logger.warn("Failed to delete token for openHAB. Response code is {}",
deleteTokenResponse.getStatus());
return;
}
logger.debug("Successfully deleted token for openHAB from controller");
} catch (NanoleafUnauthorizedException e) {
logger.warn("Attempt to delete token for openHAB failed. Token unauthorized.");
} catch (NanoleafException ne) {
logger.warn("Attempt to delete token for openHAB failed : {}", ne.getMessage());
}
logger.debug("Successfully deleted token for openHAB from controller");
} catch (NanoleafUnauthorizedException e) {
logger.warn("Attempt to delete token for openHAB failed. Token unauthorized.");
} catch (NanoleafException ne) {
logger.warn("Attempt to delete token for openHAB failed : {}", ne.getMessage());
}
stopAllJobs();
super.handleRemoval();
logger.debug("Nanoleaf controller removed");
stopAllJobs();
super.handleRemoval();
logger.debug("Nanoleaf controller removed");
});
}
@Override
@@ -246,34 +246,37 @@ public class NanoleafControllerHandler extends BaseBridgeHandler {
logger.debug("Disposing handler for Nanoleaf controller {}", getThing().getUID());
}
@Override
public Collection<Class<? extends ThingHandlerService>> getServices() {
return List.of(NanoleafPanelsDiscoveryService.class, NanoleafCommandDescriptionProvider.class);
}
public boolean registerControllerListener(NanoleafControllerListener controllerListener) {
logger.debug("Register new listener for controller {}", getThing().getUID());
boolean result = controllerListeners.add(controllerListener);
if (result) {
startPanelDiscoveryJob();
}
return result;
return controllerListeners.add(controllerListener);
}
public boolean unregisterControllerListener(NanoleafControllerListener controllerListener) {
logger.debug("Unregister listener for controller {}", getThing().getUID());
boolean result = controllerListeners.remove(controllerListener);
if (result) {
stopPanelDiscoveryJob();
}
return result;
return controllerListeners.remove(controllerListener);
}
public NanoleafControllerConfig getControllerConfig() {
NanoleafControllerConfig config = new NanoleafControllerConfig();
config.address = Objects.requireNonNullElse(getAddress(), "");
config.port = getPort();
config.refreshInterval = getRefreshIntervall();
config.refreshInterval = getRefreshInterval();
config.authToken = getAuthToken();
config.deviceType = Objects.requireNonNullElse(getDeviceType(), "");
return config;
}
public String getLayout() {
Layout layout = controllerInfo.getPanelLayout().getLayout();
String 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);
@@ -293,8 +296,8 @@ public class NanoleafControllerHandler extends BaseBridgeHandler {
String localAuthToken = getAuthToken();
if (localAuthToken != null && !localAuthToken.isEmpty()) {
if (updateJob == null || updateJob.isCancelled()) {
logger.debug("Start controller status job, repeat every {} sec", getRefreshIntervall());
updateJob = scheduler.scheduleWithFixedDelay(this::runUpdate, 0, getRefreshIntervall(),
logger.debug("Start controller status job, repeat every {} sec", getRefreshInterval());
updateJob = scheduler.scheduleWithFixedDelay(this::runUpdate, 0, getRefreshInterval(),
TimeUnit.SECONDS);
}
} else {
@@ -311,24 +314,6 @@ public class NanoleafControllerHandler extends BaseBridgeHandler {
}
}
public synchronized void startPanelDiscoveryJob() {
logger.debug("Starting panel discovery job. Has Controller-Listeners: {} panelDiscoveryJob: {}",
!controllerListeners.isEmpty(), panelDiscoveryJob);
if (!controllerListeners.isEmpty() && (panelDiscoveryJob == null || panelDiscoveryJob.isCancelled())) {
logger.debug("Start panel discovery job, interval={} sec", PANEL_DISCOVERY_INTERVAL);
panelDiscoveryJob = scheduler.scheduleWithFixedDelay(this::runPanelDiscovery, 0, PANEL_DISCOVERY_INTERVAL,
TimeUnit.SECONDS);
}
}
private synchronized void stopPanelDiscoveryJob() {
if (controllerListeners.isEmpty() && panelDiscoveryJob != null && !panelDiscoveryJob.isCancelled()) {
logger.debug("Stop panel discovery job");
panelDiscoveryJob.cancel(true);
this.panelDiscoveryJob = null;
}
}
private synchronized void startTouchJob() {
NanoleafControllerConfig config = getConfigAs(NanoleafControllerConfig.class);
if (!config.deviceType.equals(DEVICE_TYPE_TOUCHSUPPORT)) {
@@ -367,11 +352,7 @@ public class NanoleafControllerHandler extends BaseBridgeHandler {
try {
updateFromControllerInfo();
startTouchJob(); // if device type has changed, start touch detection.
// controller might have been offline, e.g. for firmware update. In this case, return to online state
if (ThingStatus.OFFLINE.equals(getThing().getStatus())) {
logger.debug("Controller {} is back online", thing.getUID());
updateStatus(ThingStatus.ONLINE);
}
updateStatus(ThingStatus.ONLINE);
} catch (NanoleafUnauthorizedException nae) {
logger.warn("Status update unauthorized: {}", nae.getMessage());
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
@@ -403,7 +384,8 @@ public class NanoleafControllerHandler extends BaseBridgeHandler {
return;
}
ContentResponse authTokenResponse = OpenAPIUtils
.requestBuilder(httpClient, getControllerConfig(), API_ADD_USER, HttpMethod.POST).send();
.requestBuilder(httpClient, getControllerConfig(), API_ADD_USER, HttpMethod.POST)
.timeout(20, TimeUnit.SECONDS).send();
if (logger.isTraceEnabled()) {
logger.trace("Auth token response: {}", authTokenResponse.getContentAsString());
}
@@ -429,7 +411,6 @@ public class NanoleafControllerHandler extends BaseBridgeHandler {
stopPairingJob();
startUpdateJob();
startPanelDiscoveryJob();
startTouchJob();
} else {
logger.debug("No auth token found in response: {}", authTokenResponse.getContentAsString());
@@ -446,7 +427,7 @@ public class NanoleafControllerHandler extends BaseBridgeHandler {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"@text/error.nanoleaf.controller.noTokenReceived");
} catch (InterruptedException | ExecutionException | TimeoutException e) {
logger.warn("Cannot send authorization request to controller: ", e);
logger.debug("Cannot send authorization request to controller: ", e);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"@text/error.nanoleaf.controller.authRequest");
} catch (RuntimeException e) {
@@ -459,34 +440,6 @@ public class NanoleafControllerHandler extends BaseBridgeHandler {
}
}
private void runPanelDiscovery() {
logger.debug("Run panel discovery job");
// Trigger a new discovery of connected panels
for (NanoleafControllerListener controllerListener : controllerListeners) {
try {
controllerListener.onControllerInfoFetched(getThing().getUID(), receiveControllerInfo());
} catch (NanoleafUnauthorizedException nue) {
logger.warn("Panel discovery unauthorized: {}", nue.getMessage());
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"@text/error.nanoleaf.controller.invalidToken");
String localAuthToken = getAuthToken();
if (localAuthToken == null || localAuthToken.isEmpty()) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING,
"@text/error.nanoleaf.controller.noToken");
}
} catch (NanoleafInterruptedException nie) {
logger.info("Panel discovery has been stopped.");
} catch (NanoleafException ne) {
logger.warn("Failed to discover panels: ", ne);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"@text/error.nanoleaf.controller.communication");
} catch (RuntimeException e) {
logger.warn("Panel discovery job failed", e);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "@text/error.nanoleaf.controller.runtime");
}
}
}
/**
* This is based on the touch event detection described in https://forum.nanoleaf.me/docs/openapi#_842h3097vbgq
*/
@@ -579,14 +532,9 @@ public class NanoleafControllerHandler extends BaseBridgeHandler {
private void updateFromControllerInfo() throws NanoleafException {
logger.debug("Update channels for controller {}", thing.getUID());
this.controllerInfo = receiveControllerInfo();
if (controllerInfo == null) {
logger.debug("No Controller Info has been provided");
return;
}
final State state = controllerInfo.getState();
OnOffType powerState = state.getOnOff();
updateState(CHANNEL_POWER, powerState);
@Nullable
Ct colorTemperature = state.getColorTemperature();
@@ -662,6 +610,10 @@ public class NanoleafControllerHandler extends BaseBridgeHandler {
panelHandler.updatePanelColorChannel();
}
});
for (NanoleafControllerListener controllerListener : controllerListeners) {
controllerListener.onControllerInfoFetched(getThing().getUID(), controllerInfo);
}
}
private ControllerInfo receiveControllerInfo() throws NanoleafException, NanoleafUnauthorizedException {
@@ -674,17 +626,6 @@ public class NanoleafControllerHandler extends BaseBridgeHandler {
private void sendStateCommand(String channel, Command command) throws NanoleafException {
State stateObject = new State();
switch (channel) {
case CHANNEL_POWER:
if (command instanceof OnOffType) {
// On/Off command - turns controller on/off
BooleanState state = new On();
state.setValue(OnOffType.ON.equals(command));
stateObject.setState(state);
} else {
logger.warn("Unhandled command type: {}", command.getClass().getName());
return;
}
break;
case CHANNEL_COLOR:
if (command instanceof OnOffType) {
// On/Off command - turns controller on/off
@@ -780,13 +721,6 @@ public class NanoleafControllerHandler extends BaseBridgeHandler {
return;
}
break;
case CHANNEL_PANEL_LAYOUT:
@Nullable
Layout layout = controllerInfo.getPanelLayout().getLayout();
String layoutView = (layout != null) ? layout.getLayoutView() : "";
logger.info("Panel layout and ids for controller {} \n{}", thing.getUID(), layoutView);
updateState(CHANNEL_PANEL_LAYOUT, OnOffType.OFF);
break;
default:
logger.warn("Unhandled command type: {}", command.getClass().getName());
return;
@@ -844,7 +778,7 @@ public class NanoleafControllerHandler extends BaseBridgeHandler {
this.port = port;
}
private int getRefreshIntervall() {
private int getRefreshInterval() {
return refreshIntervall;
}
@@ -871,7 +805,6 @@ public class NanoleafControllerHandler extends BaseBridgeHandler {
private void stopAllJobs() {
stopPairingJob();
stopUpdateJob();
stopPanelDiscoveryJob();
stopTouchJob();
}
}

View File

@@ -15,11 +15,11 @@ package org.openhab.binding.nanoleaf.internal.handler;
import static org.openhab.binding.nanoleaf.internal.NanoleafBindingConstants.*;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
@@ -28,13 +28,21 @@ import org.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.client.util.StringContentProvider;
import org.eclipse.jetty.http.HttpMethod;
import org.openhab.binding.nanoleaf.internal.*;
import org.openhab.binding.nanoleaf.internal.NanoleafBadRequestException;
import org.openhab.binding.nanoleaf.internal.NanoleafException;
import org.openhab.binding.nanoleaf.internal.NanoleafNotFoundException;
import org.openhab.binding.nanoleaf.internal.NanoleafUnauthorizedException;
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.library.types.*;
import org.openhab.core.library.types.HSBType;
import org.openhab.core.library.types.IncreaseDecreaseType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.PercentType;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.CommonTriggerEvents;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
@@ -165,8 +173,9 @@ public class NanoleafPanelHandler extends BaseThingHandler {
private void sendRenderedEffectCommand(Command command) throws NanoleafException {
logger.debug("Command Type: {}", command.getClass());
HSBType currentPanelColor = getPanelColor();
if (currentPanelColor != null)
if (currentPanelColor != null) {
logger.debug("currentPanelColor: {}", currentPanelColor.toString());
}
HSBType newPanelColor = new HSBType();
if (command instanceof HSBType) {
@@ -205,11 +214,11 @@ public class NanoleafPanelHandler extends BaseThingHandler {
// transform to RGB
PercentType[] rgbPercent = newPanelColor.toRGB();
logger.trace("Setting new rgbpercent {} {} {}", rgbPercent[0], rgbPercent[1], rgbPercent[2]);
int red = rgbPercent[0].toBigDecimal().divide(BigDecimal.valueOf(100), 2, BigDecimal.ROUND_HALF_UP)
int red = rgbPercent[0].toBigDecimal().divide(BigDecimal.valueOf(100), 2, RoundingMode.HALF_UP)
.multiply(new BigDecimal(255)).intValue();
int green = rgbPercent[1].toBigDecimal().divide(BigDecimal.valueOf(100), 2, BigDecimal.ROUND_HALF_UP)
int green = rgbPercent[1].toBigDecimal().divide(BigDecimal.valueOf(100), 2, RoundingMode.HALF_UP)
.multiply(new BigDecimal(255)).intValue();
int blue = rgbPercent[2].toBigDecimal().divide(BigDecimal.valueOf(100), 2, BigDecimal.ROUND_HALF_UP)
int blue = rgbPercent[2].toBigDecimal().divide(BigDecimal.valueOf(100), 2, RoundingMode.HALF_UP)
.multiply(new BigDecimal(255)).intValue();
logger.trace("Setting new rgb {} {} {}", red, green, blue);
Bridge bridge = getBridge();
@@ -253,8 +262,9 @@ public class NanoleafPanelHandler extends BaseThingHandler {
@Nullable
HSBType panelColor = getPanelColor();
logger.trace("updatePanelColorChannel: panelColor: {}", panelColor);
if (panelColor != null)
if (panelColor != null) {
updateState(CHANNEL_PANEL_COLOR, panelColor);
}
}
/**
@@ -265,28 +275,14 @@ public class NanoleafPanelHandler extends BaseThingHandler {
public void updatePanelGesture(int gesture) {
switch (gesture) {
case 0:
updateState(CHANNEL_PANEL_SINGLE_TAP, OnOffType.ON);
singleTapJob = scheduler.schedule(this::resetSingleTap, 1, TimeUnit.SECONDS);
logger.debug("Asserting single tap of panel {} to ON", getPanelID());
triggerChannel(CHANNEL_PANEL_TAP, CommonTriggerEvents.SHORT_PRESSED);
break;
case 1:
updateState(CHANNEL_PANEL_DOUBLE_TAP, OnOffType.ON);
doubleTapJob = scheduler.schedule(this::resetDoubleTap, 1, TimeUnit.SECONDS);
logger.debug("Asserting double tap of panel {} to ON", getPanelID());
triggerChannel(CHANNEL_PANEL_TAP, CommonTriggerEvents.DOUBLE_PRESSED);
break;
}
}
private void resetSingleTap() {
updateState(CHANNEL_PANEL_SINGLE_TAP, OnOffType.OFF);
logger.debug("Resetting single tap of panel {} to OFF", getPanelID());
}
private void resetDoubleTap() {
updateState(CHANNEL_PANEL_DOUBLE_TAP, OnOffType.OFF);
logger.debug("Resetting double tap of panel {} to OFF", getPanelID());
}
public String getPanelID() {
String panelID = getThing().getConfiguration().get(CONFIG_PANEL_ID).toString();
return panelID;
@@ -327,7 +323,7 @@ public class NanoleafPanelHandler extends BaseThingHandler {
} catch (NanoleafException nue) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"@text/error.nanoleaf.panel.communication");
logger.warn("Panel data could not be retrieved: {}", nue.getMessage());
logger.debug("Panel data could not be retrieved: {}", nue.getMessage());
}
return panelInfo.get(panelID);

View File

@@ -60,7 +60,7 @@ public class Layout {
* Returns an text representation for a canvas layout.
*
* Note only canvas supported currently due to its easy geometry
*
*
* @return a String containing the layout
*/
public String getLayoutView() {
@@ -97,6 +97,11 @@ public class Layout {
int shiftWidth = getSideLength() / 2;
if (shiftWidth == 0) {
// seems we do not have squares here
return "Cannot render layout. Please note that layout views are only supported for square panels.";
}
int lineY = maxy;
Map<Integer, PositionDatum> map;
@@ -108,8 +113,9 @@ public class Layout {
@Nullable
PositionDatum panel = positionData.get(index);
if (panel != null && panel.getPosY() == lineY)
if (panel != null && panel.getPosY() == lineY) {
map.put(panel.getPosX(), panel);
}
}
}
lineY -= shiftWidth;
@@ -118,14 +124,16 @@ public class Layout {
@Nullable
PositionDatum panel = map.get(x);
view += String.format("%5s ", panel.getPanelId());
} else
} else {
view += " ";
}
}
view += "\n";
view += System.lineSeparator();
}
return view;
} else
} else {
return "";
}
}
}

View File

@@ -25,7 +25,7 @@
<description>@text/thing-type.config.nanoleaf.controller.refreshInterval.description</description>
<default>60</default>
</parameter>
<parameter name="deviceType" type="text" readOnly="true">
<parameter name="deviceType" type="text">
<label>@text/thing-type.config.nanoleaf.controller.deviceType.label</label>
<description>@text/thing-type.config.nanoleaf.controller.deviceType.description</description>
<default>lightPanels</default>

View File

@@ -1,5 +1,5 @@
binding.nanoleaf.name = Nanoleaf Binding
binding.nanoleaf.description = Integrates the Nanoleaf Light Panels (v010221)
binding.nanoleaf.description = Integrates the Nanoleaf light panels
# thing types
thing-type.nanoleaf.controller.name = Nanoleaf Controller
@@ -22,14 +22,6 @@ thing-type.config.nanoleaf.lightpanel.id.label = ID Of The Panel
thing-type.config.nanoleaf.lightpanel.id.description = The ID of the panel assigned by the controller
# channel types
channel-type.nanoleaf.power.label = Power
channel-type.nanoleaf.power.description = Power state of the controller
channel-type.nanoleaf.color.label = Color
channel-type.nanoleaf.color.description = Color setting for an individual or all panels
channel-type.nanoleaf.colorTemperature.label = Color Temperature
channel-type.nanoleaf.colorTemperature.description = Color temperature in percent
channel-type.nanoleaf.colorTemperatureAbs.label = Color Temperature (K)
channel-type.nanoleaf.colorTemperatureAbs.description = Color temperature in Kelvin (K)
channel-type.nanoleaf.colorMode.label = Color Mode
channel-type.nanoleaf.colorMode.description = Current color mode: Effect, hue/saturation or color temperature
channel-type.nanoleaf.effect.label = Effect
@@ -40,21 +32,17 @@ channel-type.nanoleaf.rhythmActive.label = Rhythm Active
channel-type.nanoleaf.rhythmActive.description = Activity state of the rhythm module
channel-type.nanoleaf.rhythmMode.label = Rhythm Mode
channel-type.nanoleaf.rhythmMode.description = Sound source for the rhythm module (microphone or aux cable)
channel-type.nanoleaf.panelLayout.label = Panel Layout
channel-type.nanoleaf.panelLayout.description = Creates a panel layout upon request
channel-type.nanoleaf.panelColor.label = Panel Color
channel-type.nanoleaf.panelColor.description = Color of the individual panel
channel-type.nanoleaf.singleTap.label = SingleTap
channel-type.nanoleaf.singleTap.description = Single tap on the panel
channel-type.nanoleaf.doubleTap.label = DoubleTap
channel-type.nanoleaf.doubleTap.description = Double tap on the panel
channel-type.nanoleaf.tap.label = Button
channel-type.nanoleaf.tap.description = Button events of the panel
# error messages
error.nanoleaf.controller.noIp = IP/host address and/or port are not configured for the controller.
error.nanoleaf.controller.incompatibleFirmware = Incompatible controller firmware. Remove the device, update the firmware, and add it again.
error.nanoleaf.controller.noToken = No authorization token found. To start pairing, press the on-off button of the controller for 5-7 seconds until the LED starts flashing in a pattern.
error.nanoleaf.controller.invalidToken = Invalid token. Replace with valid token or start pairing again by removing the invalid token from the configuration.
error.nanoleaf.controller.communication = Communication failed. Please check configuration.
error.nanoleaf.controller.communication = Communication failed. Please check your network and configuration.
error.nanoleaf.controller.pairingFailed = Pairing failed. Press the on-off button for 5-7 seconds until the LED starts flashing in a pattern.
error.nanoleaf.controller.invalidData = Pairing failed. Received invalid data
error.nanoleaf.controller.noTokenReceived = Pairing failed. No authorization token received from controller.

View File

@@ -22,14 +22,6 @@ thing-type.config.nanoleaf.lightpanel.id.label = Paneel ID
thing-type.config.nanoleaf.lightpanel.id.description = Vom Controller vergebene ID eines Paneels
# channel types
channel-type.nanoleaf.power.label = Power
channel-type.nanoleaf.power.description = Ermöglicht das An- und Ausschalten des Nanoleaf Light Panels
channel-type.nanoleaf.color.label = Farbe
channel-type.nanoleaf.color.description = Farbe aller oder eines einzelnen Paneels
channel-type.nanoleaf.colorTemperature.label = Farbtemperatur
channel-type.nanoleaf.colorTemperature.description = Farbtemperatur in Prozent
channel-type.nanoleaf.colorTemperatureAbs.label = Farbtemperatur (K)
channel-type.nanoleaf.colorTemperatureAbs.description = Farbtemperatur in Kelvin (K)
channel-type.nanoleaf.colorMode.label = Farbmodus
channel-type.nanoleaf.colorMode.description = Effekt, Hue/Sat oder Farbtemperatur für alle Paneele.
channel-type.nanoleaf.effect.label = Effekt
@@ -40,21 +32,17 @@ channel-type.nanoleaf.rhythmActive.label = Rhythm Aktiv
channel-type.nanoleaf.rhythmActive.description = Zeigt an ob das Mikrofon des Rhythm Modules ativ ist.
channel-type.nanoleaf.rhythmMode.label = Rhythm Modus
channel-type.nanoleaf.rhythmMode.description = Erlaubt den Wechsel zwischen eingebautem Mikrofon und AUX-Kabel.
channel-type.nanoleaf.panelLayout.label = PanelLayout
channel-type.nanoleaf.panelLayout.description = Erzeugt auf Anfrage ein Panel-Layout
channel-type.nanoleaf.panelColor.label = Paneelfarbe
channel-type.nanoleaf.panelColor.description = Farbe des einzelnen Paneels
channel-type.nanoleaf.singleTap.label = Einzel-Tap
channel-type.nanoleaf.singleTap.description = Panel wurde einmal angetippt
channel-type.nanoleaf.doubleTap.label = Doppel-Tap
channel-type.nanoleaf.doubleTap.description = Panel wurde zweimal hintereinander angetippt
channel-type.nanoleaf.tap.label = Taster
channel-type.nanoleaf.tap.description = Tastevents des Panels
# error messages
error.nanoleaf.controller.noIp = IP/Host-Adresse und/oder Port sind für den Controller nicht konfiguriert.
error.nanoleaf.controller.incompatibleFirmware = Inkompatible Controller-Firmware. Firmware aktualisieren und das Gerät neu hinzufügen.
error.nanoleaf.controller.noToken = Kein Authentifizierungstoken gefunden. Um das Pairing zu starten, den An/Aus-Knopf am Controller für 5-7 Sekunden lang gedrückt halten, bis die LED anfängt zu blinken.
error.nanoleaf.controller.invalidToken = Ungültiges Authentifizierungstoken. Token ändern oder das Pairing neu starten durch Löschen des ungültigen Tokens.
error.nanoleaf.controller.communication = Kommunikationsfehler. Konfiguration des Controllers prüfen.
error.nanoleaf.controller.communication = Kommunikationsfehler. Netzwerk und Konfiguration des Controllers prüfen.
error.nanoleaf.controller.pairingFailed = Pairing fehlgeschlagen. Um das Pairing zu starten, den An/Aus-Knopf am Controller für 5-7 Sekunden lang gedrückt halten, bis die LED anfängt zu blinken.
error.nanoleaf.controller.invalidData = Pairing fehlgeschlagen. Ungültige Daten vom Controller empfangen.
error.nanoleaf.controller.noTokenReceived = Pairing fehlgeschlagen. Kein Authentifizierungstoken empfangen.

View File

@@ -9,16 +9,14 @@
<description>@text/thing-type.nanoleaf.controller.description</description>
<channels>
<channel id="power" typeId="power"/>
<channel id="color" typeId="color"/>
<channel id="colorTemperature" typeId="colorTemperature"/>
<channel id="colorTemperatureAbs" typeId="colorTemperatureAbs"/>
<channel id="color" typeId="system.color"/>
<channel id="colorTemperature" typeId="system.color-temperature"/>
<channel id="colorTemperatureAbs" typeId="system.color-temperature-abs"/>
<channel id="colorMode" typeId="colorMode"/>
<channel id="effect" typeId="effect"/>
<channel id="rhythmState" typeId="rhythmState"/>
<channel id="rhythmActive" typeId="rhythmActive"/>
<channel id="rhythmMode" typeId="rhythmMode"/>
<channel id="panelLayout" typeId="panelLayout"/>
</channels>
<properties>
@@ -40,9 +38,8 @@
<label>@text/thing-type.nanoleaf.lightpanel.name</label>
<description>@text/thing-type.nanoleaf.lightpanel.description</description>
<channels>
<channel id="panelColor" typeId="color"/>
<channel id="singleTap" typeId="singleTap"/>
<channel id="doubleTap" typeId="doubleTap"/>
<channel id="color" typeId="system.color"/>
<channel id="tap" typeId="system.button"/>
</channels>
<representation-property>id</representation-property>
@@ -50,35 +47,13 @@
<config-description-ref uri="thing-type:nanoleaf:lightpanel"/>
</thing-type>
<channel-type id="power">
<item-type>Switch</item-type>
<label>@text/channel-type.nanoleaf.power.label</label>
<description>@text/channel-type.nanoleaf.power.description</description>
<category>Switch</category>
<state readOnly="false">
<options>
<option value="ON">On</option>
<option value="OFF">Off</option>
</options>
</state>
<channel-type id="effect">
<item-type>String</item-type>
<label>@text/channel-type.nanoleaf.effect.label</label>
<description>@text/channel-type.nanoleaf.effect.description</description>
</channel-type>
<channel-type id="colorTemperature">
<item-type>Dimmer</item-type>
<label>@text/channel-type.nanoleaf.colorTemperature.label</label>
<description>@text/channel-type.nanoleaf.colorTemperature.description</description>
<state min="0" max="100" step="1"/>
</channel-type>
<channel-type id="colorTemperatureAbs">
<item-type>Number</item-type>
<label>@text/channel-type.nanoleaf.colorTemperatureAbs.label</label>
<description>@text/channel-type.nanoleaf.colorTemperatureAbs.description</description>
<category>ColorLight</category>
<state min="1200" max="6500" step="100"/>
</channel-type>
<channel-type id="colorMode">
<channel-type id="colorMode" advanced="true">
<item-type>String</item-type>
<label>@text/channel-type.nanoleaf.colorMode.label</label>
<description>@text/channel-type.nanoleaf.colorMode.description</description>
@@ -91,62 +66,30 @@
</state>
</channel-type>
<channel-type id="color">
<item-type>Color</item-type>
<label>@text/channel-type.nanoleaf.color.label</label>
<description>@text/Color of the Light Panels</description>
</channel-type>
<channel-type id="effect">
<item-type>String</item-type>
<label>@text/channel-type.nanoleaf.effect.label</label>
<description>@text/channel-type.nanoleaf.effect.description</description>
</channel-type>
<channel-type id="rhythmState">
<channel-type id="rhythmState" advanced="true">
<item-type>Switch</item-type>
<label>@text/channel-type.nanoleaf.rhythmState.label</label>
<description>@text/channel-type.nanoleaf.rhythmState.description</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="rhythmActive">
<channel-type id="rhythmActive" advanced="true">
<item-type>Switch</item-type>
<label>@text/channel-type.nanoleaf.rhythmActive.label</label>
<description>@text/channel-type.nanoleaf.rhythmActive.description</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="rhythmMode">
<channel-type id="rhythmMode" advanced="true">
<item-type>Number</item-type>
<label>@text/channel-type.nanoleaf.rhythmMode.label</label>
<description>@text/channel-type.nanoleaf.rhythmMode.description</description>
<state min="0" max="1">
<options>
<option value="0">Microphone</option>
<option value="1">Aux cable</option>
<option value="1">Aux Cable</option>
</options>
</state>
</channel-type>
<channel-type id="singleTap">
<item-type>Switch</item-type>
<label>@text/channel-type.nanoleaf.singleTap.label</label>
<description>@text/channel-type.nanoleaf.singleTap.description</description>
<state readOnly="false"/>
</channel-type>
<channel-type id="doubleTap">
<item-type>Switch</item-type>
<label>@text/channel-type.nanoleaf.doubleTap.label</label>
<description>@text/channel-type.nanoleaf.doubleTap.description</description>
<state readOnly="false"/>
</channel-type>
<channel-type id="panelLayout">
<item-type>Switch</item-type>
<label>@text/channel-type.nanoleaf.panelLayout.label</label>
<description>@text/channel-type.nanoleaf.panelLayout.description</description>
<state readOnly="false"/>
</channel-type>
</thing:thing-descriptions>