[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 ## 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. 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 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. With the controller thing you can control channels which affect all panels, e.g. selecting effects or setting the brightness.
The lightpanel (singular) thing controls one of the individual panels/canvas that are connected to each other. The lightpanel (singular) thing controls one of the individual panels/canvas that are connected to each other.
Each individual panel has therefore its own id assigned to it. Each individual panel has therefore its own id assigned to it.
You can set the **color** for each panel 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 | | Nanoleaf Name | Type | Description | supported | touch support |
| ---------------------- | ---- | ---------------------------------------------------------- | --------- | ------------- | | ---------------------- | ---- | ---------------------------------------------------------- | --------- | ------------- |
| Light Panels | NL22 | Triangles 1st Generation | X | (-) | | Light Panels | NL22 | Triangles 1st Generation | X | (-) |
| Shapes Triangle | NL42 | Triangles 2nd Generation (rounded edges) | X | X | | Shapes Triangle | NL42 | Triangles 2nd Generation (rounded edges) | X | X |
| Shapes Hexagon | NL42 | Triangles 2nd Generation (rounded edges) | (X) | (X) | | Shapes Hexagon | NL42 | Hexagons | X | X |
| Shapes Mini Triangles | ?? | Mini Triangles | ? | ? | | Shapes Mini Triangles | ?? | Mini Triangles | ? | ? |
| Canvas | NL29 | Squares | X | X | | Canvas | NL29 | Squares | X | X |
x = Supported (x) = Supported but only tested by community (-) = unknown (no device available to test) x = Supported (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 ## Discovery
**Adding the Controller as a Thing** **Adding the Controller as a Thing**
@@ -73,13 +68,16 @@ In this case:
- stop and then start openHAB - stop and then start openHAB
- Rediscover like described above - 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)
- Set up a switch item with the channel panelLayout on the controller (see NanoRetrieveLayout below) and set the switch to true For canvas that use square panels, you can request the layout through a console command:
- 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 Compare the following output with the right picture at the beginning of the article
@@ -94,8 +92,6 @@ Compare the following output with the right picture at the beginning of the arti
``` ```
Disclaimer: this works best with square devices and not necessarily well with triangles due to the more geometrically flexible layout.
## Thing Configuration ## Thing Configuration
The controller thing has the following parameters: 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 | | port | Port number of the light panels contoller. Default is 16021 |
| authToken | The authentication token received from the controller after successful pairing. | | 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. | | 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: 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) | | 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. 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 ## Channels
@@ -123,8 +119,7 @@ The controller bridge has the following channels:
| Channel | Item Type | Description | Read Only | | Channel | Item Type | Description | Read Only |
|---------------------|-----------|------------------------------------------------------------------------|-----------| |---------------------|-----------|------------------------------------------------------------------------|-----------|
| power | Switch | Power state of the light panels | No | | color | Color | Color, power and brightness of all light panels | No |
| color | Color | Color of all light panels | No |
| colorTemperature | Dimmer | Color temperature (in percent) of all light panels | No | | colorTemperature | Dimmer | Color temperature (in percent) of all light panels | No |
| colorTemperatureAbs | Number | Color temperature (in Kelvin, 1200 to 6500) of all light panels | No | | colorTemperatureAbs | Number | Color temperature (in Kelvin, 1200 to 6500) of all light panels | No |
| colorMode | String | Color mode of the light panels | Yes | | colorMode | String | Color mode of the light panels | Yes |
@@ -132,48 +127,39 @@ The controller bridge has the following channels:
| rhythmState | Switch | Connection state of the rhythm module | Yes | | rhythmState | Switch | Connection state of the rhythm module | Yes |
| rhythmActive | Switch | Activity state of the rhythm module | Yes | | rhythmActive | Switch | Activity state of the rhythm module | Yes |
| rhythmMode | Number | Sound source for the rhythm module. 0=Microphone, 1=Aux cable | No | | rhythmMode | Number | Sound source for the rhythm module. 0=Microphone, 1=Aux cable | No |
| panelLayout | Switch | Set to true will log out panel layout (returns to off automatically | No |
A lightpanel thing has the following channels: 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 | | color | 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 | | tap | Trigger | [Canvas Only] Sends events of gestures. Currently, these are SHORT_PRESSED and DOUBLE_PRESSED events. | Yes |
| doubleTap | Switch | [Canvas Only] Is set when the user taps that panel twice (1 second pulse) | Yes |
**color and panelColor** The color channels support full color control with hue, saturation and brightness values.
The color and panelColor channels support full color control with hue, saturation and brightness values.
For example, brightness of *all* panels at once can be controlled by defining a dimmer item for the color channel of the *controller thing*. For example, brightness of *all* panels at once can be controlled by defining a dimmer item for the color channel of the *controller thing*.
The same applies to the panelColor channel of an individual lightpanel thing. The same applies to the color channel of an individual lightpanel.
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.
**Limitations assigning specific colors on individual panels:** **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. - 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 - 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.
- 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. - 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
- 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 first panelColor command is applied to that panel (and of course then all subsequent commands)
- 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.
- 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** **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. 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. 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 Keep in mind that the double tap is used as an already built-in functionality by default when you buy the nanoleaf: it switches all panels (hence the controller) to on or off like a light switch for all the panels at once. To circumvent that
- Within the nanoleaf app go to the dashboard and choose your device. Enter the settings for that device by clicking the cog icon in the upper right corner. - Within the nanoleaf app go to the dashboard and choose your device. Enter the settings for that device by clicking the cog icon in the upper right corner.
- Enable "Touch Gesture" and assign the gestures you want to happen but set the double tap to unassigned. - Enable "Touch Gesture" 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. 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 * the [address="..." ] of the controller
* and the [id=123] of the lightpanel * 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 ### 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 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. - 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" } Color NanoleafColor "Color" { channel="nanoleaf:controller:MyLightPanels:color" }
Dimmer NanoleafBrightness "Brightness [%.0f]" { channel="nanoleaf:controller:MyLightPanels:color" } Dimmer NanoleafBrightness "Brightness [%.0f]" { channel="nanoleaf:controller:MyLightPanels:color" }
String NanoleafHue "Hue [%s]" 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 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" } Switch NanoleafRhythmActive "Rhythm active [MAP(nanoleaf.map):%s]" { channel="nanoleaf:controller:MyLightPanels:rhythmActive" }
Number NanoleafRhythmSource "Rhythm source [%s]" { channel="nanoleaf:controller:MyLightPanels:rhythmMode" } 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 // 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" } Color PanelColor "Panel 1" { channel="nanoleaf:lightpanel:MyLightPanels:135:color" }
Dimmer Panel1Brightness "Panel 1" { channel="nanoleaf:lightpanel:MyLightPanels:135:panelColor" } Dimmer Panel1Brightness "Panel 1" { channel="nanoleaf:lightpanel:MyLightPanels:135:color" }
Switch Panel1DoubleTap "Toggle device on and off" { channel="nanoleaf:lightpanel:MyLightPanels:135:doubleTap" } Switch Panel2Color "Panel 2" { channel="nanoleaf:lightpanel:MyLightPanels:158:color" }
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" }
Switch NanoleafRainbowScene "Show Rainbow Scene" Switch NanoleafRainbowScene "Show Rainbow Scene"
``` ```
@@ -252,11 +234,10 @@ sitemap nanoleaf label="Nanoleaf"
Slider item=NanoleafColorTemp Slider item=NanoleafColorTemp
Setpoint item=NanoleafColorTempAbs step=100 minValue=1200 maxValue=6500 Setpoint item=NanoleafColorTempAbs step=100 minValue=1200 maxValue=6500
Text item=NanoleafColorMode 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=NanoleafRhythmState
Text item=NanoleafRhythmActive Text item=NanoleafRhythmActive
Selection item=NanoleafRhythmSource mappings=[0="Microphone", 1="Aux"] Selection item=NanoleafRhythmSource mappings=[0="Microphone", 1="Aux"]
Switch item=NanoRetrieveLayout
} }
Frame label="Panels" { 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 ### nanoleaf.rules
``` ```
@@ -313,8 +291,7 @@ end
rule "Nanoleaf canvas touch detection Panel 2" rule "Nanoleaf canvas touch detection Panel 2"
when when
Item Panel2SingleTap changed from NULL to ON or Channel "nanoleaf:lightpanel:MyLightPanels:158:tap" triggered SHORT_PRESS
Item Panel2SingleTap changed from OFF to ON
then then
logInfo("CanvasTouch", "Nanoleaf Canvas Panel 2 was touched once") logInfo("CanvasTouch", "Nanoleaf Canvas Panel 2 was touched once")
@@ -327,8 +304,7 @@ end
rule "Nanoleaf double tap toggles power of device" rule "Nanoleaf double tap toggles power of device"
when when
Item Panel1DoubleTap changed from NULL to ON or Channel "nanoleaf:lightpanel:MyLightPanels:135:tap" triggered DOUBLE_PRESS
Item Panel1DoubleTap changed from OFF to ON
then then
logInfo("CanvasTouch", "Nanoleaf Canvas Panel 1 was touched twice. Toggle Power of whole canvas.") 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 @NonNullByDefault
public class NanoleafBindingConstants { public class NanoleafBindingConstants {
private static final String BINDING_ID = "nanoleaf"; public static final String BINDING_ID = "nanoleaf";
// List of all Thing Type UIDs // List of all Thing Type UIDs
public static final ThingTypeUID THING_TYPE_CONTROLLER = new ThingTypeUID(BINDING_ID, "controller"); 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"; public static final String CONFIG_PANEL_ID = "id";
// List of controller channels // List of controller channels
public static final String CHANNEL_POWER = "power";
public static final String CHANNEL_COLOR = "color"; public static final String CHANNEL_COLOR = "color";
public static final String CHANNEL_COLOR_TEMPERATURE = "colorTemperature"; public static final String CHANNEL_COLOR_TEMPERATURE = "colorTemperature";
public static final String CHANNEL_COLOR_TEMPERATURE_ABS = "colorTemperatureAbs"; 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_STATE = "rhythmState";
public static final String CHANNEL_RHYTHM_ACTIVE = "rhythmActive"; public static final String CHANNEL_RHYTHM_ACTIVE = "rhythmActive";
public static final String CHANNEL_RHYTHM_MODE = "rhythmMode"; public static final String CHANNEL_RHYTHM_MODE = "rhythmMode";
public static final String CHANNEL_PANEL_LAYOUT = "panelLayout";
// List of light panel channels // List of light panel channels
public static final String CHANNEL_PANEL_COLOR = "panelColor"; public static final String CHANNEL_PANEL_COLOR = "color";
public static final String CHANNEL_PANEL_SINGLE_TAP = "singleTap"; public static final String CHANNEL_PANEL_TAP = "tap";
public static final String CHANNEL_PANEL_DOUBLE_TAP = "doubleTap";
// Nanoleaf OpenAPI URLs // Nanoleaf OpenAPI URLs
public static final String API_V1_BASE_URL = "/api/v1"; 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 static org.openhab.binding.nanoleaf.internal.NanoleafBindingConstants.*;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream; 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.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient; 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.NanoleafControllerHandler;
import org.openhab.binding.nanoleaf.internal.handler.NanoleafPanelHandler; 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.io.net.http.HttpClientFactory;
import org.openhab.core.thing.Bridge; import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.Thing; import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID; 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.BaseThingHandlerFactory;
import org.openhab.core.thing.binding.ThingHandler; import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerFactory; 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.Activate;
import org.osgi.service.component.annotations.Component; import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference; import org.osgi.service.component.annotations.Reference;
@@ -49,6 +42,7 @@ import org.slf4j.LoggerFactory;
* and panel (thing) handlers. * and panel (thing) handlers.
* *
* @author Martin Raepple - Initial contribution * @author Martin Raepple - Initial contribution
* @author Kai Kreuzer - made discovery a handler service
*/ */
@NonNullByDefault @NonNullByDefault
@Component(configurationPid = "binding.nanoleaf", service = ThingHandlerFactory.class) @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())); .unmodifiableSet(Stream.of(THING_TYPE_LIGHT_PANEL, THING_TYPE_CONTROLLER).collect(Collectors.toSet()));
private final Logger logger = LoggerFactory.getLogger(NanoleafHandlerFactory.class); private final Logger logger = LoggerFactory.getLogger(NanoleafHandlerFactory.class);
private final Map<ThingUID, ServiceRegistration<?>> discoveryServiceRegs = new HashMap<>();
private final HttpClient httpClient; private final HttpClient httpClient;
@Activate @Activate
@@ -77,7 +70,6 @@ public class NanoleafHandlerFactory extends BaseThingHandlerFactory {
if (THING_TYPE_CONTROLLER.equals(thingTypeUID)) { if (THING_TYPE_CONTROLLER.equals(thingTypeUID)) {
NanoleafControllerHandler handler = new NanoleafControllerHandler((Bridge) thing, httpClient); NanoleafControllerHandler handler = new NanoleafControllerHandler((Bridge) thing, httpClient);
registerDiscoveryService(handler);
logger.debug("Nanoleaf controller handler created."); logger.debug("Nanoleaf controller handler created.");
return handler; return handler;
} else if (THING_TYPE_LIGHT_PANEL.equals(thingTypeUID)) { } else if (THING_TYPE_LIGHT_PANEL.equals(thingTypeUID)) {
@@ -87,30 +79,4 @@ public class NanoleafHandlerFactory extends BaseThingHandlerFactory {
} }
return null; 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.nio.charset.StandardCharsets;
import java.util.Iterator; import java.util.Iterator;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException; import java.util.concurrent.TimeoutException;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
@@ -57,7 +58,7 @@ public class OpenAPIUtils {
LOGGER.trace("RequestBuilder: Sending Request {}:{} {} ", requestURI.getHost(), requestURI.getPort(), LOGGER.trace("RequestBuilder: Sending Request {}:{} {} ", requestURI.getHost(), requestURI.getPort(),
requestURI.getPath()); 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) 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, logger.trace("Discovered nanoleaf host: {} port: {} firmWare: {} modelId: {} qualifiedName: {}", host, port,
firmwareVersion, modelId, qualifiedName); 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); firmwareVersion, host, port);
if (!OpenAPIUtils.checkRequiredFirmware(service.getPropertyString("md"), firmwareVersion)) { 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); 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.DiscoveryResult;
import org.openhab.core.config.discovery.DiscoveryResultBuilder; import org.openhab.core.config.discovery.DiscoveryResultBuilder;
import org.openhab.core.thing.ThingUID; import org.openhab.core.thing.ThingUID;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerService;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@@ -41,36 +43,37 @@ import org.slf4j.LoggerFactory;
* panels connected to the controller. * panels connected to the controller.
* *
* @author Martin Raepple - Initial contribution * @author Martin Raepple - Initial contribution
* @author Kai Kreuzer - Made it a ThingHandlerService
*/ */
@NonNullByDefault @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 static final int SEARCH_TIMEOUT_SECONDS = 60;
private final Logger logger = LoggerFactory.getLogger(NanoleafPanelsDiscoveryService.class); 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. * Constructs a new {@link NanoleafPanelsDiscoveryService}.
*
* @param nanoleafControllerHandler The bridge handler this discovery service is attached to
*/ */
public NanoleafPanelsDiscoveryService(NanoleafControllerHandler nanoleafControllerHandler) { public NanoleafPanelsDiscoveryService() {
super(NanoleafHandlerFactory.SUPPORTED_THING_TYPES_UIDS, SEARCH_TIMEOUT_SECONDS, false); 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 @Override
protected void startScan() { protected void startScan() {
logger.debug("Starting Nanoleaf panel discovery"); logger.debug("Starting Nanoleaf panel discovery");
bridgeHandler.registerControllerListener(this); createResultsFromControllerInfo();
}
@Override
protected synchronized void stopScan() {
logger.debug("Stopping Nanoleaf panel discovery");
super.stopScan();
bridgeHandler.unregisterControllerListener(this);
} }
/** /**
@@ -81,38 +84,60 @@ public class NanoleafPanelsDiscoveryService extends AbstractDiscoveryService imp
*/ */
@Override @Override
public void onControllerInfoFetched(ThingUID bridge, ControllerInfo controllerInfo) { public void onControllerInfoFetched(ThingUID bridge, ControllerInfo controllerInfo) {
logger.debug("Discover panels connected to controller with id {}", bridge.getAsString()); this.controllerInfo = controllerInfo;
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");
}
private void createResultsFromControllerInfo() {
ThingUID bridgeUID;
if (bridgeHandler != null) {
bridgeUID = bridgeHandler.getThing().getUID();
} else { } 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.net.URI;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets; 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.CopyOnWriteArrayList;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import java.util.concurrent.ScheduledFuture; import java.util.concurrent.ScheduledFuture;
@@ -36,10 +40,11 @@ import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.HttpStatus; import org.eclipse.jetty.http.HttpStatus;
import org.openhab.binding.nanoleaf.internal.NanoleafControllerListener; import org.openhab.binding.nanoleaf.internal.NanoleafControllerListener;
import org.openhab.binding.nanoleaf.internal.NanoleafException; import org.openhab.binding.nanoleaf.internal.NanoleafException;
import org.openhab.binding.nanoleaf.internal.NanoleafInterruptedException;
import org.openhab.binding.nanoleaf.internal.NanoleafUnauthorizedException; import org.openhab.binding.nanoleaf.internal.NanoleafUnauthorizedException;
import org.openhab.binding.nanoleaf.internal.OpenAPIUtils; 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.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.AuthToken;
import org.openhab.binding.nanoleaf.internal.model.BooleanState; import org.openhab.binding.nanoleaf.internal.model.BooleanState;
import org.openhab.binding.nanoleaf.internal.model.Brightness; 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.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail; import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.binding.BaseBridgeHandler; 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.Command;
import org.openhab.core.types.RefreshType; import org.openhab.core.types.RefreshType;
import org.slf4j.Logger; import org.slf4j.Logger;
@@ -81,15 +87,13 @@ import com.google.gson.JsonSyntaxException;
* *
* @author Martin Raepple - Initial contribution * @author Martin Raepple - Initial contribution
* @author Stefan Höhn - Canvas Touch Support * @author Stefan Höhn - Canvas Touch Support
* @author Kai Kreuzer - refactoring, bug fixing and code clean up
*/ */
@NonNullByDefault @NonNullByDefault
public class NanoleafControllerHandler extends BaseBridgeHandler { public class NanoleafControllerHandler extends BaseBridgeHandler {
// Pairing interval in seconds // Pairing interval in seconds
private static final int PAIRING_INTERVAL = 25; private static final int PAIRING_INTERVAL = 10;
// Panel discovery interval in seconds
private static final int PANEL_DISCOVERY_INTERVAL = 30;
private final Logger logger = LoggerFactory.getLogger(NanoleafControllerHandler.class); private final Logger logger = LoggerFactory.getLogger(NanoleafControllerHandler.class);
private HttpClient httpClient; private HttpClient httpClient;
@@ -98,7 +102,6 @@ public class NanoleafControllerHandler extends BaseBridgeHandler {
// Pairing, update and panel discovery jobs and touch event job // Pairing, update and panel discovery jobs and touch event job
private @NonNullByDefault({}) ScheduledFuture<?> pairingJob; private @NonNullByDefault({}) ScheduledFuture<?> pairingJob;
private @NonNullByDefault({}) ScheduledFuture<?> updateJob; private @NonNullByDefault({}) ScheduledFuture<?> updateJob;
private @NonNullByDefault({}) ScheduledFuture<?> panelDiscoveryJob;
private @NonNullByDefault({}) ScheduledFuture<?> touchJob; private @NonNullByDefault({}) ScheduledFuture<?> touchJob;
// JSON parser for API responses // JSON parser for API responses
@@ -120,7 +123,7 @@ public class NanoleafControllerHandler extends BaseBridgeHandler {
@Override @Override
public void initialize() { public void initialize() {
logger.debug("Initializing the controller (bridge)"); logger.debug("Initializing the controller (bridge)");
updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.BRIDGE_UNINITIALIZED); updateStatus(ThingStatus.UNKNOWN);
NanoleafControllerConfig config = getConfigAs(NanoleafControllerConfig.class); NanoleafControllerConfig config = getConfigAs(NanoleafControllerConfig.class);
setAddress(config.address); setAddress(config.address);
setPort(config.port); setPort(config.port);
@@ -157,13 +160,9 @@ public class NanoleafControllerHandler extends BaseBridgeHandler {
"@text/error.nanoleaf.controller.noToken"); "@text/error.nanoleaf.controller.noToken");
startPairingJob(); startPairingJob();
stopUpdateJob(); stopUpdateJob();
stopPanelDiscoveryJob();
} else { } else {
logger.debug("Controller is online. Stop pairing job, start update & panel discovery jobs");
updateStatus(ThingStatus.ONLINE);
stopPairingJob(); stopPairingJob();
startUpdateJob(); startUpdateJob();
startPanelDiscoveryJob();
startTouchJob(); startTouchJob();
} }
} catch (IllegalArgumentException iae) { } catch (IllegalArgumentException iae) {
@@ -186,11 +185,9 @@ public class NanoleafControllerHandler extends BaseBridgeHandler {
updateFromControllerInfo(); updateFromControllerInfo();
} else { } else {
switch (channelUID.getId()) { switch (channelUID.getId()) {
case CHANNEL_POWER:
case CHANNEL_COLOR: case CHANNEL_COLOR:
case CHANNEL_COLOR_TEMPERATURE: case CHANNEL_COLOR_TEMPERATURE:
case CHANNEL_COLOR_TEMPERATURE_ABS: case CHANNEL_COLOR_TEMPERATURE_ABS:
case CHANNEL_PANEL_LAYOUT:
sendStateCommand(channelUID.getId(), command); sendStateCommand(channelUID.getId(), command);
break; break;
case CHANNEL_EFFECT: case CHANNEL_EFFECT:
@@ -218,25 +215,28 @@ public class NanoleafControllerHandler extends BaseBridgeHandler {
@Override @Override
public void handleRemoval() { public void handleRemoval() {
// delete token for openHAB scheduler.execute(() -> {
ContentResponse deleteTokenResponse; // delete token for openHAB
try { ContentResponse deleteTokenResponse;
Request deleteTokenRequest = OpenAPIUtils.requestBuilder(httpClient, getControllerConfig(), API_DELETE_USER, try {
HttpMethod.DELETE); Request deleteTokenRequest = OpenAPIUtils.requestBuilder(httpClient, getControllerConfig(),
deleteTokenResponse = OpenAPIUtils.sendOpenAPIRequest(deleteTokenRequest); API_DELETE_USER, HttpMethod.DELETE);
if (deleteTokenResponse.getStatus() != HttpStatus.NO_CONTENT_204) { deleteTokenResponse = OpenAPIUtils.sendOpenAPIRequest(deleteTokenRequest);
logger.warn("Failed to delete token for openHAB. Response code is {}", deleteTokenResponse.getStatus()); if (deleteTokenResponse.getStatus() != HttpStatus.NO_CONTENT_204) {
return; 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"); stopAllJobs();
} catch (NanoleafUnauthorizedException e) { super.handleRemoval();
logger.warn("Attempt to delete token for openHAB failed. Token unauthorized."); logger.debug("Nanoleaf controller removed");
} catch (NanoleafException ne) { });
logger.warn("Attempt to delete token for openHAB failed : {}", ne.getMessage());
}
stopAllJobs();
super.handleRemoval();
logger.debug("Nanoleaf controller removed");
} }
@Override @Override
@@ -246,34 +246,37 @@ public class NanoleafControllerHandler extends BaseBridgeHandler {
logger.debug("Disposing handler for Nanoleaf controller {}", getThing().getUID()); 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) { public boolean registerControllerListener(NanoleafControllerListener controllerListener) {
logger.debug("Register new listener for controller {}", getThing().getUID()); logger.debug("Register new listener for controller {}", getThing().getUID());
boolean result = controllerListeners.add(controllerListener); return controllerListeners.add(controllerListener);
if (result) {
startPanelDiscoveryJob();
}
return result;
} }
public boolean unregisterControllerListener(NanoleafControllerListener controllerListener) { public boolean unregisterControllerListener(NanoleafControllerListener controllerListener) {
logger.debug("Unregister listener for controller {}", getThing().getUID()); logger.debug("Unregister listener for controller {}", getThing().getUID());
boolean result = controllerListeners.remove(controllerListener); return controllerListeners.remove(controllerListener);
if (result) {
stopPanelDiscoveryJob();
}
return result;
} }
public NanoleafControllerConfig getControllerConfig() { public NanoleafControllerConfig getControllerConfig() {
NanoleafControllerConfig config = new NanoleafControllerConfig(); NanoleafControllerConfig config = new NanoleafControllerConfig();
config.address = Objects.requireNonNullElse(getAddress(), ""); config.address = Objects.requireNonNullElse(getAddress(), "");
config.port = getPort(); config.port = getPort();
config.refreshInterval = getRefreshIntervall(); config.refreshInterval = getRefreshInterval();
config.authToken = getAuthToken(); config.authToken = getAuthToken();
config.deviceType = Objects.requireNonNullElse(getDeviceType(), ""); config.deviceType = Objects.requireNonNullElse(getDeviceType(), "");
return config; return config;
} }
public String getLayout() {
Layout layout = controllerInfo.getPanelLayout().getLayout();
String layoutView = (layout != null) ? layout.getLayoutView() : "";
return layoutView;
}
public synchronized void startPairingJob() { public synchronized void startPairingJob() {
if (pairingJob == null || pairingJob.isCancelled()) { if (pairingJob == null || pairingJob.isCancelled()) {
logger.debug("Start pairing job, interval={} sec", PAIRING_INTERVAL); logger.debug("Start pairing job, interval={} sec", PAIRING_INTERVAL);
@@ -293,8 +296,8 @@ public class NanoleafControllerHandler extends BaseBridgeHandler {
String localAuthToken = getAuthToken(); String localAuthToken = getAuthToken();
if (localAuthToken != null && !localAuthToken.isEmpty()) { if (localAuthToken != null && !localAuthToken.isEmpty()) {
if (updateJob == null || updateJob.isCancelled()) { if (updateJob == null || updateJob.isCancelled()) {
logger.debug("Start controller status job, repeat every {} sec", getRefreshIntervall()); logger.debug("Start controller status job, repeat every {} sec", getRefreshInterval());
updateJob = scheduler.scheduleWithFixedDelay(this::runUpdate, 0, getRefreshIntervall(), updateJob = scheduler.scheduleWithFixedDelay(this::runUpdate, 0, getRefreshInterval(),
TimeUnit.SECONDS); TimeUnit.SECONDS);
} }
} else { } 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() { private synchronized void startTouchJob() {
NanoleafControllerConfig config = getConfigAs(NanoleafControllerConfig.class); NanoleafControllerConfig config = getConfigAs(NanoleafControllerConfig.class);
if (!config.deviceType.equals(DEVICE_TYPE_TOUCHSUPPORT)) { if (!config.deviceType.equals(DEVICE_TYPE_TOUCHSUPPORT)) {
@@ -367,11 +352,7 @@ public class NanoleafControllerHandler extends BaseBridgeHandler {
try { try {
updateFromControllerInfo(); updateFromControllerInfo();
startTouchJob(); // if device type has changed, start touch detection. 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 updateStatus(ThingStatus.ONLINE);
if (ThingStatus.OFFLINE.equals(getThing().getStatus())) {
logger.debug("Controller {} is back online", thing.getUID());
updateStatus(ThingStatus.ONLINE);
}
} catch (NanoleafUnauthorizedException nae) { } catch (NanoleafUnauthorizedException nae) {
logger.warn("Status update unauthorized: {}", nae.getMessage()); logger.warn("Status update unauthorized: {}", nae.getMessage());
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
@@ -403,7 +384,8 @@ public class NanoleafControllerHandler extends BaseBridgeHandler {
return; return;
} }
ContentResponse authTokenResponse = OpenAPIUtils 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()) { if (logger.isTraceEnabled()) {
logger.trace("Auth token response: {}", authTokenResponse.getContentAsString()); logger.trace("Auth token response: {}", authTokenResponse.getContentAsString());
} }
@@ -429,7 +411,6 @@ public class NanoleafControllerHandler extends BaseBridgeHandler {
stopPairingJob(); stopPairingJob();
startUpdateJob(); startUpdateJob();
startPanelDiscoveryJob();
startTouchJob(); startTouchJob();
} else { } else {
logger.debug("No auth token found in response: {}", authTokenResponse.getContentAsString()); 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, updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"@text/error.nanoleaf.controller.noTokenReceived"); "@text/error.nanoleaf.controller.noTokenReceived");
} catch (InterruptedException | ExecutionException | TimeoutException e) { } 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, updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"@text/error.nanoleaf.controller.authRequest"); "@text/error.nanoleaf.controller.authRequest");
} catch (RuntimeException e) { } 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 * 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 { private void updateFromControllerInfo() throws NanoleafException {
logger.debug("Update channels for controller {}", thing.getUID()); logger.debug("Update channels for controller {}", thing.getUID());
this.controllerInfo = receiveControllerInfo(); this.controllerInfo = receiveControllerInfo();
if (controllerInfo == null) {
logger.debug("No Controller Info has been provided");
return;
}
final State state = controllerInfo.getState(); final State state = controllerInfo.getState();
OnOffType powerState = state.getOnOff(); OnOffType powerState = state.getOnOff();
updateState(CHANNEL_POWER, powerState);
@Nullable @Nullable
Ct colorTemperature = state.getColorTemperature(); Ct colorTemperature = state.getColorTemperature();
@@ -662,6 +610,10 @@ public class NanoleafControllerHandler extends BaseBridgeHandler {
panelHandler.updatePanelColorChannel(); panelHandler.updatePanelColorChannel();
} }
}); });
for (NanoleafControllerListener controllerListener : controllerListeners) {
controllerListener.onControllerInfoFetched(getThing().getUID(), controllerInfo);
}
} }
private ControllerInfo receiveControllerInfo() throws NanoleafException, NanoleafUnauthorizedException { private ControllerInfo receiveControllerInfo() throws NanoleafException, NanoleafUnauthorizedException {
@@ -674,17 +626,6 @@ public class NanoleafControllerHandler extends BaseBridgeHandler {
private void sendStateCommand(String channel, Command command) throws NanoleafException { private void sendStateCommand(String channel, Command command) throws NanoleafException {
State stateObject = new State(); State stateObject = new State();
switch (channel) { 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: case CHANNEL_COLOR:
if (command instanceof OnOffType) { if (command instanceof OnOffType) {
// On/Off command - turns controller on/off // On/Off command - turns controller on/off
@@ -780,13 +721,6 @@ public class NanoleafControllerHandler extends BaseBridgeHandler {
return; return;
} }
break; 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: default:
logger.warn("Unhandled command type: {}", command.getClass().getName()); logger.warn("Unhandled command type: {}", command.getClass().getName());
return; return;
@@ -844,7 +778,7 @@ public class NanoleafControllerHandler extends BaseBridgeHandler {
this.port = port; this.port = port;
} }
private int getRefreshIntervall() { private int getRefreshInterval() {
return refreshIntervall; return refreshIntervall;
} }
@@ -871,7 +805,6 @@ public class NanoleafControllerHandler extends BaseBridgeHandler {
private void stopAllJobs() { private void stopAllJobs() {
stopPairingJob(); stopPairingJob();
stopUpdateJob(); stopUpdateJob();
stopPanelDiscoveryJob();
stopTouchJob(); stopTouchJob();
} }
} }

View File

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

View File

@@ -97,6 +97,11 @@ public class Layout {
int shiftWidth = getSideLength() / 2; 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; int lineY = maxy;
Map<Integer, PositionDatum> map; Map<Integer, PositionDatum> map;
@@ -108,8 +113,9 @@ public class Layout {
@Nullable @Nullable
PositionDatum panel = positionData.get(index); PositionDatum panel = positionData.get(index);
if (panel != null && panel.getPosY() == lineY) if (panel != null && panel.getPosY() == lineY) {
map.put(panel.getPosX(), panel); map.put(panel.getPosX(), panel);
}
} }
} }
lineY -= shiftWidth; lineY -= shiftWidth;
@@ -118,14 +124,16 @@ public class Layout {
@Nullable @Nullable
PositionDatum panel = map.get(x); PositionDatum panel = map.get(x);
view += String.format("%5s ", panel.getPanelId()); view += String.format("%5s ", panel.getPanelId());
} else } else {
view += " "; view += " ";
}
} }
view += "\n"; view += System.lineSeparator();
} }
return view; return view;
} else } else {
return ""; return "";
}
} }
} }

View File

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

View File

@@ -1,5 +1,5 @@
binding.nanoleaf.name = Nanoleaf Binding 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 types
thing-type.nanoleaf.controller.name = Nanoleaf Controller 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 thing-type.config.nanoleaf.lightpanel.id.description = The ID of the panel assigned by the controller
# channel types # 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.label = Color Mode
channel-type.nanoleaf.colorMode.description = Current color mode: Effect, hue/saturation or color temperature channel-type.nanoleaf.colorMode.description = Current color mode: Effect, hue/saturation or color temperature
channel-type.nanoleaf.effect.label = Effect 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.rhythmActive.description = Activity state of the rhythm module
channel-type.nanoleaf.rhythmMode.label = Rhythm Mode 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.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.label = Panel Color
channel-type.nanoleaf.panelColor.description = Color of the individual panel channel-type.nanoleaf.panelColor.description = Color of the individual panel
channel-type.nanoleaf.singleTap.label = SingleTap channel-type.nanoleaf.tap.label = Button
channel-type.nanoleaf.singleTap.description = Single tap on the panel channel-type.nanoleaf.tap.description = Button events of the panel
channel-type.nanoleaf.doubleTap.label = DoubleTap
channel-type.nanoleaf.doubleTap.description = Double tap on the panel
# error messages # error messages
error.nanoleaf.controller.noIp = IP/host address and/or port are not configured for the controller. error.nanoleaf.controller.noIp = IP/host address and/or port are not configured for the controller.
error.nanoleaf.controller.incompatibleFirmware = Incompatible controller firmware. Remove the device, update the firmware, and add it again. 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.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.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.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.invalidData = Pairing failed. Received invalid data
error.nanoleaf.controller.noTokenReceived = Pairing failed. No authorization token received from controller. 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 thing-type.config.nanoleaf.lightpanel.id.description = Vom Controller vergebene ID eines Paneels
# channel types # 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.label = Farbmodus
channel-type.nanoleaf.colorMode.description = Effekt, Hue/Sat oder Farbtemperatur für alle Paneele. channel-type.nanoleaf.colorMode.description = Effekt, Hue/Sat oder Farbtemperatur für alle Paneele.
channel-type.nanoleaf.effect.label = Effekt 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.rhythmActive.description = Zeigt an ob das Mikrofon des Rhythm Modules ativ ist.
channel-type.nanoleaf.rhythmMode.label = Rhythm Modus channel-type.nanoleaf.rhythmMode.label = Rhythm Modus
channel-type.nanoleaf.rhythmMode.description = Erlaubt den Wechsel zwischen eingebautem Mikrofon und AUX-Kabel. 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.label = Paneelfarbe
channel-type.nanoleaf.panelColor.description = Farbe des einzelnen Paneels channel-type.nanoleaf.panelColor.description = Farbe des einzelnen Paneels
channel-type.nanoleaf.singleTap.label = Einzel-Tap channel-type.nanoleaf.tap.label = Taster
channel-type.nanoleaf.singleTap.description = Panel wurde einmal angetippt channel-type.nanoleaf.tap.description = Tastevents des Panels
channel-type.nanoleaf.doubleTap.label = Doppel-Tap
channel-type.nanoleaf.doubleTap.description = Panel wurde zweimal hintereinander angetippt
# error messages # error messages
error.nanoleaf.controller.noIp = IP/Host-Adresse und/oder Port sind für den Controller nicht konfiguriert. error.nanoleaf.controller.noIp = IP/Host-Adresse und/oder Port sind für den Controller nicht konfiguriert.
error.nanoleaf.controller.incompatibleFirmware = Inkompatible Controller-Firmware. Firmware aktualisieren und das Gerät neu hinzufügen. 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.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.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.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.invalidData = Pairing fehlgeschlagen. Ungültige Daten vom Controller empfangen.
error.nanoleaf.controller.noTokenReceived = Pairing fehlgeschlagen. Kein Authentifizierungstoken empfangen. error.nanoleaf.controller.noTokenReceived = Pairing fehlgeschlagen. Kein Authentifizierungstoken empfangen.

View File

@@ -9,16 +9,14 @@
<description>@text/thing-type.nanoleaf.controller.description</description> <description>@text/thing-type.nanoleaf.controller.description</description>
<channels> <channels>
<channel id="power" typeId="power"/> <channel id="color" typeId="system.color"/>
<channel id="color" typeId="color"/> <channel id="colorTemperature" typeId="system.color-temperature"/>
<channel id="colorTemperature" typeId="colorTemperature"/> <channel id="colorTemperatureAbs" typeId="system.color-temperature-abs"/>
<channel id="colorTemperatureAbs" typeId="colorTemperatureAbs"/>
<channel id="colorMode" typeId="colorMode"/> <channel id="colorMode" typeId="colorMode"/>
<channel id="effect" typeId="effect"/> <channel id="effect" typeId="effect"/>
<channel id="rhythmState" typeId="rhythmState"/> <channel id="rhythmState" typeId="rhythmState"/>
<channel id="rhythmActive" typeId="rhythmActive"/> <channel id="rhythmActive" typeId="rhythmActive"/>
<channel id="rhythmMode" typeId="rhythmMode"/> <channel id="rhythmMode" typeId="rhythmMode"/>
<channel id="panelLayout" typeId="panelLayout"/>
</channels> </channels>
<properties> <properties>
@@ -40,9 +38,8 @@
<label>@text/thing-type.nanoleaf.lightpanel.name</label> <label>@text/thing-type.nanoleaf.lightpanel.name</label>
<description>@text/thing-type.nanoleaf.lightpanel.description</description> <description>@text/thing-type.nanoleaf.lightpanel.description</description>
<channels> <channels>
<channel id="panelColor" typeId="color"/> <channel id="color" typeId="system.color"/>
<channel id="singleTap" typeId="singleTap"/> <channel id="tap" typeId="system.button"/>
<channel id="doubleTap" typeId="doubleTap"/>
</channels> </channels>
<representation-property>id</representation-property> <representation-property>id</representation-property>
@@ -50,35 +47,13 @@
<config-description-ref uri="thing-type:nanoleaf:lightpanel"/> <config-description-ref uri="thing-type:nanoleaf:lightpanel"/>
</thing-type> </thing-type>
<channel-type id="power"> <channel-type id="effect">
<item-type>Switch</item-type> <item-type>String</item-type>
<label>@text/channel-type.nanoleaf.power.label</label> <label>@text/channel-type.nanoleaf.effect.label</label>
<description>@text/channel-type.nanoleaf.power.description</description> <description>@text/channel-type.nanoleaf.effect.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>
<channel-type id="colorTemperature"> <channel-type id="colorMode" advanced="true">
<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">
<item-type>String</item-type> <item-type>String</item-type>
<label>@text/channel-type.nanoleaf.colorMode.label</label> <label>@text/channel-type.nanoleaf.colorMode.label</label>
<description>@text/channel-type.nanoleaf.colorMode.description</description> <description>@text/channel-type.nanoleaf.colorMode.description</description>
@@ -91,62 +66,30 @@
</state> </state>
</channel-type> </channel-type>
<channel-type id="color"> <channel-type id="rhythmState" advanced="true">
<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">
<item-type>Switch</item-type> <item-type>Switch</item-type>
<label>@text/channel-type.nanoleaf.rhythmState.label</label> <label>@text/channel-type.nanoleaf.rhythmState.label</label>
<description>@text/channel-type.nanoleaf.rhythmState.description</description> <description>@text/channel-type.nanoleaf.rhythmState.description</description>
<state readOnly="true"/> <state readOnly="true"/>
</channel-type> </channel-type>
<channel-type id="rhythmActive"> <channel-type id="rhythmActive" advanced="true">
<item-type>Switch</item-type> <item-type>Switch</item-type>
<label>@text/channel-type.nanoleaf.rhythmActive.label</label> <label>@text/channel-type.nanoleaf.rhythmActive.label</label>
<description>@text/channel-type.nanoleaf.rhythmActive.description</description> <description>@text/channel-type.nanoleaf.rhythmActive.description</description>
<state readOnly="true"/> <state readOnly="true"/>
</channel-type> </channel-type>
<channel-type id="rhythmMode"> <channel-type id="rhythmMode" advanced="true">
<item-type>Number</item-type> <item-type>Number</item-type>
<label>@text/channel-type.nanoleaf.rhythmMode.label</label> <label>@text/channel-type.nanoleaf.rhythmMode.label</label>
<description>@text/channel-type.nanoleaf.rhythmMode.description</description> <description>@text/channel-type.nanoleaf.rhythmMode.description</description>
<state min="0" max="1"> <state min="0" max="1">
<options> <options>
<option value="0">Microphone</option> <option value="0">Microphone</option>
<option value="1">Aux cable</option> <option value="1">Aux Cable</option>
</options> </options>
</state> </state>
</channel-type> </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> </thing:thing-descriptions>