## 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**
- 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.
-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)
+For canvas that use square panels, you can request the layout through a console command:
-- 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
+```
+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
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:
| 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:
| 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
| 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 |
| 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 and panelColor**
+| 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 |
-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.
}
```
-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
### 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]"
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"
```
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" {
}
```
-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
```
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")
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.")
@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");
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";
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";
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;
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;
* 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)
.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
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)) {
}
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.");
- }
}
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;
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)
--- /dev/null
+/**
+ * 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."));
+ }
+}
--- /dev/null
+/**
+ * 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);
+ }
+ }
+}
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);
}
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;
* 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
- protected void startScan() {
- logger.debug("Starting Nanoleaf panel discovery");
- bridgeHandler.registerControllerListener(this);
+ public void deactivate() {
+ if (bridgeHandler != null) {
+ bridgeHandler.unregisterControllerListener(this);
+ }
+ super.deactivate();
}
@Override
- protected synchronized void stopScan() {
- logger.debug("Stopping Nanoleaf panel discovery");
- super.stopScan();
- bridgeHandler.unregisterControllerListener(this);
+ protected void startScan() {
+ logger.debug("Starting Nanoleaf panel discovery");
+ createResultsFromControllerInfo();
}
/**
*/
@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();
+ this.controllerInfo = controllerInfo;
+ }
- if (layout != null && layout.getNumPanels() > 0) {
+ private void createResultsFromControllerInfo() {
+ ThingUID bridgeUID;
+ if (bridgeHandler != null) {
+ bridgeUID = bridgeHandler.getThing().getUID();
+ } else {
+ return;
+ }
+ if (controllerInfo != null) {
+ final PanelLayout panelLayout = controllerInfo.getPanelLayout();
@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);
+ 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("Couldn't add panels to inbox as layout position data was null");
+ logger.debug("No panels found or connected to controller");
}
-
- } else {
- logger.info("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;
+ }
}
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;
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;
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;
*
* @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;
// 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
@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);
"@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) {
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:
@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
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);
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 {
}
}
- 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)) {
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,
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());
}
stopPairingJob();
startUpdateJob();
- startPanelDiscoveryJob();
startTouchJob();
} else {
logger.debug("No auth token found in response: {}", authTokenResponse.getContentAsString());
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) {
}
}
- 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
*/
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();
panelHandler.updatePanelColorChannel();
}
});
+
+ for (NanoleafControllerListener controllerListener : controllerListeners) {
+ controllerListener.onControllerInfoFetched(getThing().getUID(), controllerInfo);
+ }
}
private ControllerInfo receiveControllerInfo() throws NanoleafException, NanoleafUnauthorizedException {
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
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;
this.port = port;
}
- private int getRefreshIntervall() {
+ private int getRefreshInterval() {
return refreshIntervall;
}
private void stopAllJobs() {
stopPairingJob();
stopUpdateJob();
- stopPanelDiscoveryJob();
stopTouchJob();
}
}
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;
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;
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) {
// 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();
@Nullable
HSBType panelColor = getPanelColor();
logger.trace("updatePanelColorChannel: panelColor: {}", panelColor);
- if (panelColor != null)
+ if (panelColor != null) {
updateState(CHANNEL_PANEL_COLOR, panelColor);
+ }
}
/**
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;
} 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);
* 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() {
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;
@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;
@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 "";
+ }
}
}
<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>
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
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
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.
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
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.
<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>
<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>
<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>
-
- <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 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="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>
</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>