added migrated 2.x add-ons

Signed-off-by: Kai Kreuzer <kai@openhab.org>
This commit is contained in:
Kai Kreuzer
2020-09-21 01:58:32 +02:00
parent bbf1a7fd29
commit 6df6783b60
11662 changed files with 1302875 additions and 11 deletions

View File

@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="src" output="target/classes" path="src/main/java">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry excluding="**" kind="src" output="target/classes" path="src/main/resources">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="src" output="target/test-classes" path="src/test/java">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
<attribute name="test" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-11">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="output" path="target/classes"/>
</classpath>

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>org.openhab.binding.nanoleaf</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.m2e.core.maven2Builder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.jdt.core.javanature</nature>
<nature>org.eclipse.m2e.core.maven2Nature</nature>
</natures>
</projectDescription>

View File

@@ -0,0 +1,13 @@
This content is produced and maintained by the openHAB project.
* Project home: https://www.openhab.org
== Declared Project Licenses
This program and the accompanying materials are made available under the terms
of the Eclipse Public License 2.0 which is available at
https://www.eclipse.org/legal/epl-2.0/.
== Source Code
https://github.com/openhab/openhab-addons

View File

@@ -0,0 +1,338 @@
# Nanoleaf Binding
This binding integrates the [Nanoleaf Light Panels](https://nanoleaf.me/en/consumer-led-lighting/products/smarter-series/nanoleaf-light-panels-smarter-kit/).
![Image](doc/Nanoleaf.jpg)
It enables you to authenticate, control, and obtain information of a Light Panel's device.
The binding uses the [Nanoleaf OpenAPI](https://forum.nanoleaf.me/docs/openapi), which requires firmware version [1.5.0](https://helpdesk.nanoleaf.me/hc/en-us/articles/214006129-Light-Panels-Firmware-Release-Notes) or higher.
![Image](doc/LightPanels2_small.jpg) ![Image](doc/NanoCanvas_small.jpg)
## Supported Things
Currently Nanoleaf's "Light Panels" and "Canvas" devices are supported. Note that only the canvas type does support the touch functionality.
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).
With the controller thing you can control channels which affect all panels, e.g. selecting effects or setting the brightness.
The lightpanel (singular) thing controls one of the individual panels/canvas that are connected to each other. Each individual panel has therefore its own id assigned to it.
You can set the **color** for each panel or turn it on (white) or off (black) and in the case of a nanoleaf canvas you can even detect single and double **touch events** related to an individual panel which opens a whole new world of controlling any other device within your openHAB environment.
Note: In case of major changes of a binding (like adding more features to a thing) it becomes necessary to delete your things due to the things not being compatible anymore. Don't worry too much though as they will be easily redetected and nothing really is lost. Just make sure that you delete them and rediscover as described below.
## Discovery
**Adding the Controller as a Thing**
To add a nanoleaf controller you need to go to your inbox in Paper UI, press the plus sign which **starts a scan** (you can of course use habmin for that as well). Then choose "Nanoleaf Binding".
A controller (also known as bridge) device is discovered automatically through mDNS in the local network.
Alternatively, you can also provide a things file (see below for more details).
After the device is discovered and added as a thing, it needs a valid authentication token that must be obtained by pairing it with your openHAB instance.
Without the token the light panels remain in status OFFLINE.
The binding supports pairing of the device with your openHAB instance as follows:
1. Make sure that the authentication token field in your Nanoleaf controller thing configuration is left empty.
2. Hold down the on-off button of the controller for 5-7 seconds until the LED starts flashing/cycling in a pattern, which turns the device in pairing mode, and openHAB will try to request an authentication token for it.
Once your openHAB instance successfully requested and stored the authentication token in the controller's thing configuration, the controller status changes to ONLINE, and you can start linking the channels to your items.
Tip: if you press (2) just before adding the item from the inbox it usually catches the auth token right away and if you are lucky it already automatically starts discovering the panels in one turn (see below).
**Adding the invidual light panels as a thing**
After you have added the controller as a thing and it has been successfully paired as described as above, the individual panels connected to it can be discovered by **starting another scan** for the Nanoleaf binding (e.g. from the Inbox in Paper UI).
All connected panels will be added as separate things to the inbox.
Troubleshooting: In seldom cases (in particular together with updating the binding) things or items do not work as expected, are offline or may not be detected. In this case
- remove the panels (maybe twice by force removing it)
- remove the controller (maybe twice by force removing it)
- stop and then start openHAB
- Rediscover like described above
**Knowing which panel has which id**
Unfortunately it is not easy to find out which panel gets which id while this is pretty important if you have lots of them and you want to assign rules to it.
Don't worry: 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)
- fire up your browser and open the openHAB server on port 9001 which shows the logs.
- Set up a switch item with the channel panelLayout on the controller (see NanoRetrieveLayout below) and set the switch to true
- look out for something like "Panel layout and ids" in the logs. Below that you will see a panel layout similar to
Compare the following output with the right picture at the beginning of the article
```
31413 9162 13276
55836 56093 48111 38724 17870 5164 64279
58086 8134 39755
41451
```
Disclaimer: this works best with square devices and not necessarily well with triangles due to the more geometrically flexible layout.
## Thing Configuration
The controller thing has the following parameters:
| Config | Description |
| --------------- | ------------------------------------------------------------------------------------- |
| address | IP address or hostname of the light panels controller (e.g. 192.168.1.100) |
| port | Port number of the light panels contoller. Default is 16021 |
| authToken | The authentication token received from the controller after successful pairing. |
| refreshInterval | Interval in seconds to refresh the state of the light panels settings. Default is 60. |
| deviceType | (readOnly) defines the type: lightpanels (triangle) or canvas (square) |
The lightpanel thing has the following parameters:
| Config | Description |
| --------------- | ------------------------------------------------------------------------------------- |
| id | ID assigned by the controller to the individual panel (e.g. 158) |
The IDs of the individual panels can be determined by starting another scan once the controller is configured and online.
This will add all connected panels with their IDs to the inbox.
## Channels
The controller bridge has the following channels:
| Channel | Item Type | Description | Read Only |
|---------------------|-----------|------------------------------------------------------------------------|-----------|
| power | Switch | Power state of the light panels | No |
| color | Color | Color of all light panels | No |
| colorTemperature | Dimmer | Color temperature (in percent) of all light panels | No |
| colorTemperatureAbs | Number | Color temperature (in Kelvin, 1200 to 6500) of all light panels | No |
| colorMode | String | Color mode of the light panels | Yes |
| effect | String | Selected effect of the light panels | No |
| rhythmState | Switch | Connection state of the rhythm module | Yes |
| rhythmActive | Switch | Activity state of the rhythm module | Yes |
| rhythmMode | Number | Sound source for the rhythm module. 0=Microphone, 1=Aux cable | No |
| panelLayout | Switch | Set to true will log out panel layout (returns to off automatically | No |
A lightpanel thing has the following channels:
| Channel | Item Type | Description | Read Only |
|---------------------|-----------|------------------------------------------------------------------------|-----------|
| panelColor | Color | Color of the individual light panel | No |
| singleTap | Switch | [Canvas Only] Is set when the user taps that panel once (1 second pulse) | Yes |
| doubleTap | Switch | [Canvas Only] Is set when the user taps that panel twice (1 second pulse) | Yes |
**color and panelColor**
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*.
The same applies to the panelColor channel of an individual lightpanel thing.
What might not be obvious and even maybe confusing is the fact that brightness and color use the *same* channel but two different *itemTypes*. While the Color-itemtype controls the color, the Dimmer-itemtype controls the brightness on the same channel.
**Limitations assigning specific colors on individual panels:**
- Due to the way the API of the nanoleaf is designed, each time a color is assigned to a panel, it will be directly sent to that panel. The result is that if you send colors to several panels more or less at the same time, they will not be set at the same time but one after the other and rather appear like a sequence but as a one shot.
- Another important limitation is that individual panels cannot be set while a dynamic effect is running on the panel which means that the following happens
- As soon as you set an individual panel a so called "static effect" is created which replaces the chosen dynamic effect. You can even see that in the nanoleaf app that shows that a static effect is now running.
- Unfortunately, at least at the moment, the colors of the current state cannot be retrieved due to the high frequency of color changes that cannot be read quickly enough from the canvas, so all panels go to OFF
- The the first panelColor command is applied to that panel (and of course then all subsequent commands)
- The fact that it is called a static effect does not mean that you cannot create animations. The Rainbow rule below shows a good example for the whole canvas. Just replace the controller item with a panel item and you will get the rainbow effect with an individual panel.
**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!
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.
These switches then act as a pulse to further control anything else via rules.
If a panel is tapped the switch is set to ON and automatically reset to OFF after 1 second (this may be configured in the future) to simulate a pulse. A rule can easily detect the transition from OFF to ON and later detect another tap as it is automatically reset by the binding. See the example below on Panel 2.
Keep in mind that the double tap is used as an already built-in functionality by default when you buy the nanoleaf: it switches all panels (hence the controller) to on or off like a light switch for all the panels at once. To circumvent that
- Within the nanoleaf app go to the dashboard and choose your device. Enter the settings for that device by clicking the cog icon in the upper right corner.
- Enable "Touch Gesture" and assign the gestures you want to happen but set the double tap to unassigned.
- To still have the possibility to switch on the whole canvas device with all its panels by double tapping a specific panel, you can easily write a rule that triggers on the double tap channel of that panel and then toggles the Power Channel of the controller. See the example below on Panel 1.
More details can be found in the full example below.
## Full Example
The following files provide a full example for a configuration (using a things file instead of automatic discovery):
### nanoleaf.things
```
Bridge nanoleaf:controller:MyLightPanels @ "mylocation" [ address="192.168.1.100", port=16021, authToken="AbcDefGhiJk879LmNopqRstUv1234WxyZ", refreshInterval=60 ] {
Thing lightpanel 135 [ id=135 ]
Thing lightpanel 158 [ id=158 ]
}
```
If you define your device statically in the thing file, autodiscovery of the same thing is suppressed by using
* the [address="..." ] of the controller
* and the [id=123] of the lightpanel
in the bracket to identify the uniqueness of the discovered device. Therefore it is recommended to the give the controller a fixed ip address.
Note: To generate the `authToken`:
* On the Nanoleaf controller, hold the on-off button for 5-7 seconds until the LED starts flashing.
* Send a POST request to the authorization endpoint within 30 seconds of activating pairing, like this:
`http://<address>:16021/api/v1/new`
e.g. via command line `curl --location --request POST 'http://<address>:16021/api/v1/new'`
### nanoleaf.items
Note: If you did autodiscover your things and items:
- A controller item looks like nanoleaf:controller:F0ED4F9351AF:power where F0ED4F9351AF is the id of the controller that has been automatically assigned by the binding.
- A panel item looks like nanoleaf:lightpanel:F0ED4F9351AF:39755:singleTap where 39755 is the id of the panel that has been automatically assigned by the binding.
```
Switch NanoleafPower "Nanoleaf" { channel="nanoleaf:controller:MyLightPanels:power" }
Color NanoleafColor "Color" { channel="nanoleaf:controller:MyLightPanels:color" }
Dimmer NanoleafBrightness "Brightness [%.0f]" { channel="nanoleaf:controller:MyLightPanels:color" }
String NanoleafHue "Hue [%s]"
String NanoleafSaturation "Saturation [%s]"
Dimmer NanoleafColorTemp "Color temperature [%.0f]" { channel="nanoleaf:controller:MyLightPanels:colorTemperature" }
Number NanoleafColorTempAbs "Color temperature [%.000f]" { channel="nanoleaf:controller:MyLightPanels:colorTemperatureAbs" }
String NanoleafColorMode "Color mode [%s]" { channel="nanoleaf:controller:MyLightPanels:colorMode" }
String NanoleafEffect "Effect" { channel="nanoleaf:controller:MyLightPanels:effect" }
Switch NanoleafRhythmState "Rhythm connected [MAP(nanoleaf.map):%s]" { channel="nanoleaf:controller:MyLightPanels:rhythmState" }
Switch NanoleafRhythmActive "Rhythm active [MAP(nanoleaf.map):%s]" { channel="nanoleaf:controller:MyLightPanels:rhythmActive" }
Number NanoleafRhythmSource "Rhythm source [%s]" { channel="nanoleaf:controller:MyLightPanels:rhythmMode" }
Switch NanoRetrieveLayout "Nano Layout" { channel="nanoleaf:controller:D81E7A7E424E:panelLayout" }
// note that the next to items use the exact same channel but the two different types Color and Dimmer to control different parameters
Color Panel1Color "Panel 1" { channel="nanoleaf:lightpanel:MyLightPanels:135:panelColor" }
Dimmer Panel1Brightness "Panel 1" { channel="nanoleaf:lightpanel:MyLightPanels:135:panelColor" }
Switch Panel1DoubleTap "Toggle device on and off" { channel="nanoleaf:lightpanel:MyLightPanels:135:doubleTap" }
Switch Panel2Color "Panel 2" { channel="nanoleaf:lightpanel:MyLightPanels:158:panelColor" }
Switch Panel2SingleTap "Panel 2 Single Tap" { channel="nanoleaf:lightpanel:MyLightPanels:158:singleTap" }
Switch Panel2DoubleTap "Panel 2 Double Tap" { channel="nanoleaf:lightpanel:MyLightPanels:158:doubleTap" }
Switch NanoleafRainbowScene "Show Rainbow Scene"
```
### nanoleaf.sitemap
```
sitemap nanoleaf label="Nanoleaf"
{
Frame label="Controller" {
Switch item=NanoleafPower
Slider item=NanoleafBrightness
Colorpicker item=NanoleafColor
Text item=NanoleafHue
Text item=NanoleafSaturation
Slider item=NanoleafColorTemp
Setpoint item=NanoleafColorTempAbs step=100 minValue=1200 maxValue=6500
Text item=NanoleafColorMode
Selection item=NanoleafEffect mappings=["Color Burst"="Color Burst", "Fireworks" = "Fireworks", "Flames" = "Flames", "Forest" = "Forest", "Inner Peace" = "Inner Peace", "Meteor Shower" = "Meteor Shower", "Nemo" = "Nemo", "Northern Lights" = "Northern Lights", "Paint Splatter" = "Paint Splatter", "Pulse Pop Beats" = "Pulse Pop Beats", "Rhythmic Northern Lights" = "Rhythmic Northern Lights", "Ripple" = "Ripple", "Romantic" = "Romantic", "Snowfall" = "Snowfall", "Sound Bar" = "Sound Bar", "Streaking Notes" = "Streaking Notes", "moonlight" = "Moonlight", "*Static*" = "Color (single panels)", "*Dynamic*" = "Color (all panels)" ]
Text item=NanoleafRhythmState
Text item=NanoleafRhythmActive
Selection item=NanoleafRhythmSource mappings=[0="Microphone", 1="Aux"]
Switch item=NanoRetrieveLayout
}
Frame label="Panels" {
Colorpicker item=Panel1Color
Slider item=Panel1Brightness
Colorpicker item=Panel2Color
}
Frame label="Scenes" {
Switch item=NanoleafRainbowScene
}
}
```
Note: The mappings to effects in the selection item are specific for each Nanoleaf installation and should be adapted accordingly.
Only the effects "\*Static\*" and "\*Dynamic\*" are predefined by the controller and should always be present in the mappings.
### nanoleaf.rules
```
rule "UpdateHueAndSat"
when Item NanoleafColor changed
then
val hsbValues = NanoleafColor.state as HSBType
NanoleafHue.postUpdate(hsbValues.hue.intValue)
NanoleafSaturation.postUpdate(hsbValues.saturation.intValue)
end
rule "ShowRainbowScene"
when Item NanoleafRainbowScene received command ON
then
val saturation = new PercentType(75)
val brightness = new PercentType(90)
val long pause = 200
var hue = 0
var direction = 1
while(NanoleafRainbowScene.state == ON) {
Thread::sleep(pause)
hue = hue + (5 * direction)
if(hue >= 359) {
hue = 359
direction = direction * -1
}
else if (hue < 0) {
hue = 0
direction = direction * -1
}
// replace NanoleafColor with Panel1Color to run rainbow on a single panel
NanoleafColor.sendCommand(new HSBType(new DecimalType(hue), saturation, brightness))
}
end
rule "Nanoleaf canvas touch detection Panel 2"
when
Item Panel2SingleTap changed from NULL to ON or
Item Panel2SingleTap changed from OFF to ON
then
logInfo("CanvasTouch", "Nanoleaf Canvas Panel 2 was touched once")
if (My_Main_Light.state == OFF) {
sendCommand(My_Main_Light,ON)
} else {
sendCommand(My_Main_Light,OFF)
}
end
rule "Nanoleaf double tap toggles power of device"
when
Item Panel1DoubleTap changed from NULL to ON or
Item Panel1DoubleTap changed from OFF to ON
then
logInfo("CanvasTouch", "Nanoleaf Canvas Panel 1 was touched twice. Toggle Power of whole canvas.")
if (NanoleafPower.state == OFF ) {
sendCommand(NanoleafPower,ON)
} else {
sendCommand(NanoleafPower,OFF)
}
end
```
### nanoleaf.map
```
ON = Yes
OFF = No
effects = Effect
hs = Hue/Saturation
ct = Color Temperature
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 240 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.addons.reactor.bundles</artifactId>
<version>3.0.0-SNAPSHOT</version>
</parent>
<artifactId>org.openhab.binding.nanoleaf</artifactId>
<name>openHAB Add-ons :: Bundles :: Nanoleaf Binding</name>
</project>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<features name="org.openhab.binding.nanoleaf-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
<repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
<feature name="openhab-binding-nanoleaf" description="Nanoleaf Binding" version="${project.version}">
<feature>openhab-runtime-base</feature>
<feature>openhab-transport-mdns</feature>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.nanoleaf/${project.version}</bundle>
</feature>
</features>

View File

@@ -0,0 +1,30 @@
/**
* Copyright (c) 2010-2020 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;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Exception if request to Nanoleaf OpenAPI does not expect the given content
*
* @author Stefan Höhn - Initial contribution
*/
@NonNullByDefault
public class NanoleafBadRequestException extends NanoleafException {
private static final long serialVersionUID = -6941678941424573256L;
public NanoleafBadRequestException(String message) {
super(message);
}
}

View File

@@ -0,0 +1,87 @@
/**
* Copyright (c) 2010-2020 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;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.ThingTypeUID;
/**
* The {@link NanoleafBindingConstants} class defines common constants, which are
* used across the whole binding.
*
* @author Martin Raepple - Initial contribution
*/
@NonNullByDefault
public class NanoleafBindingConstants {
private static final String BINDING_ID = "nanoleaf";
// List of all Thing Type UIDs
public static final ThingTypeUID THING_TYPE_CONTROLLER = new ThingTypeUID(BINDING_ID, "controller");
public static final ThingTypeUID THING_TYPE_LIGHT_PANEL = new ThingTypeUID(BINDING_ID, "lightpanel");
// Controller configuration settings
public static final String CONFIG_ADDRESS = "address";
public static final String CONFIG_PORT = "port";
public static final String CONFIG_AUTH_TOKEN = "authToken";
public static final String CONFIG_DEVICE_TYPE_CANVAS = "canvas";
public static final String CONFIG_DEVICE_TYPE_LIGHTPANELS = "lightPanels";
// Panel configuration settings
public static final String CONFIG_PANEL_ID = "id";
// List of controller channels
public static final String CHANNEL_POWER = "power";
public static final String CHANNEL_COLOR = "color";
public static final String CHANNEL_COLOR_TEMPERATURE = "colorTemperature";
public static final String CHANNEL_COLOR_TEMPERATURE_ABS = "colorTemperatureAbs";
public static final String CHANNEL_COLOR_MODE = "colorMode";
public static final String CHANNEL_EFFECT = "effect";
public static final String CHANNEL_RHYTHM_STATE = "rhythmState";
public static final String CHANNEL_RHYTHM_ACTIVE = "rhythmActive";
public static final String CHANNEL_RHYTHM_MODE = "rhythmMode";
public static final String CHANNEL_PANEL_LAYOUT = "panelLayout";
// List of light panel channels
public static final String CHANNEL_PANEL_COLOR = "panelColor";
public static final String CHANNEL_PANEL_SINGLE_TAP = "singleTap";
public static final String CHANNEL_PANEL_DOUBLE_TAP = "doubleTap";
// Nanoleaf OpenAPI URLs
public static final String API_V1_BASE_URL = "/api/v1";
public static final String API_GET_CONTROLLER_INFO = "/";
public static final String API_ADD_USER = "/new";
public static final String API_EVENTS = "/events";
public static final String API_DELETE_USER = "";
public static final String API_SET_VALUE = "/state";
public static final String API_EFFECT = "/effects";
public static final String API_RHYTHM_MODE = "/rhythm/rhythmMode";
// Nanoleaf model IDs and minimum required firmware versions
public static final String API_MIN_FW_VER_LIGHTPANELS = "1.5.0";
public static final String API_MIN_FW_VER_CANVAS = "1.1.0";
public static final String MODEL_ID_LIGHTPANELS = "NL22";
public static final String MODEL_ID_CANVAS = "NL29";
public static final String DEVICE_TYPE_LIGHTPANELS = "lightPanels";
public static final String DEVICE_TYPE_CANVAS = "canvas";
// mDNS discovery service type
// see http://forum.nanoleaf.me/docs/openapi#_gf9l5guxt8r0
public static final String SERVICE_TYPE = "_nanoleafapi._tcp.local.";
// Effect/scene name for static color
public static final String EFFECT_NAME_STATIC_COLOR = "*Dynamic*";
// Color channels increase/decrease brightness step size
public static final int BRIGHTNESS_STEP_SIZE = 5;
}

View File

@@ -0,0 +1,36 @@
/**
* Copyright (c) 2010-2020 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;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.nanoleaf.internal.model.ControllerInfo;
import org.openhab.core.thing.ThingUID;
/**
* A {@link NanoleafControllerListener} is notified by the controller thing handler.
* A listener may use it to discover additional things connected to the controller (bridge), such as individual panels.
*
* @author Martin Raepple - Initial contribution
*/
@NonNullByDefault
public interface NanoleafControllerListener {
/**
* This method is called after the bridge thing handler fetched the controller info
*
* @param bridge the Nanoleaf controller.
* @param controllerInfo the controller data with panel information
*/
void onControllerInfoFetched(ThingUID bridge, ControllerInfo controllerInfo);
}

View File

@@ -0,0 +1,37 @@
/**
* Copyright (c) 2010-2020 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;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* General binding exception if something goes wrong.
*
* @author Martin Raepple - Initial contribution
*/
@NonNullByDefault
public class NanoleafException extends Exception {
private static final long serialVersionUID = 1L;
public NanoleafException(String message) {
super(message);
}
public NanoleafException(final Throwable cause) {
super(cause);
}
public NanoleafException(final String message, final Throwable cause) {
super(message, cause);
}
}

View File

@@ -0,0 +1,116 @@
/**
* Copyright (c) 2010-2020 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;
import static org.openhab.binding.nanoleaf.internal.NanoleafBindingConstants.*;
import java.util.Collections;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.openhab.binding.nanoleaf.internal.discovery.NanoleafPanelsDiscoveryService;
import org.openhab.binding.nanoleaf.internal.handler.NanoleafControllerHandler;
import org.openhab.binding.nanoleaf.internal.handler.NanoleafPanelHandler;
import org.openhab.core.config.discovery.DiscoveryService;
import org.openhab.core.io.net.http.HttpClientFactory;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerFactory;
import org.osgi.framework.ServiceRegistration;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link NanoleafHandlerFactory} is responsible for creating the controller (bridge)
* and panel (thing) handlers.
*
* @author Martin Raepple - Initial contribution
*/
@NonNullByDefault
@Component(configurationPid = "binding.nanoleaf", service = ThingHandlerFactory.class)
public class NanoleafHandlerFactory extends BaseThingHandlerFactory {
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Collections
.unmodifiableSet(Stream.of(THING_TYPE_LIGHT_PANEL, THING_TYPE_CONTROLLER).collect(Collectors.toSet()));
private final Logger logger = LoggerFactory.getLogger(NanoleafHandlerFactory.class);
private final Map<ThingUID, ServiceRegistration<?>> discoveryServiceRegs = new HashMap<>();
private final HttpClient httpClient;
@Activate
public NanoleafHandlerFactory(@Reference final HttpClientFactory httpClientFactory) {
this.httpClient = httpClientFactory.getCommonHttpClient();
}
@Override
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
}
@Override
protected @Nullable ThingHandler createHandler(Thing thing) {
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
if (THING_TYPE_CONTROLLER.equals(thingTypeUID)) {
NanoleafControllerHandler handler = new NanoleafControllerHandler((Bridge) thing, httpClient);
registerDiscoveryService(handler);
logger.debug("Nanoleaf controller handler created.");
return handler;
} else if (THING_TYPE_LIGHT_PANEL.equals(thingTypeUID)) {
NanoleafPanelHandler handler = new NanoleafPanelHandler(thing, httpClient);
logger.debug("Nanoleaf panel handler created.");
return handler;
}
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

@@ -0,0 +1,30 @@
/**
* Copyright (c) 2010-2020 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;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Exception if request to Nanoleaf OpenAPI has been interrupted which is normally intended
*
* @author Stefan Höhn - Initial contribution
*/
@NonNullByDefault
public class NanoleafInterruptedException extends NanoleafException {
private static final long serialVersionUID = -6941678941424234257L;
public NanoleafInterruptedException(String message, InterruptedException interruptedException) {
super(message);
}
}

View File

@@ -0,0 +1,30 @@
/**
* Copyright (c) 2010-2020 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;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Exception if request to Nanoleaf OpenAPI does not return any data
*
* @author Stefan Höhn - Initial contribution
*/
@NonNullByDefault
public class NanoleafNotFoundException extends NanoleafException {
private static final long serialVersionUID = -6941678941424573256L;
public NanoleafNotFoundException(String message) {
super(message);
}
}

View File

@@ -0,0 +1,30 @@
/**
* Copyright (c) 2010-2020 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;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Exception if request to Nanoleaf OpenAPI is unauthorized (e.g. invalid or missing auth token)
*
* @author Martin Raepple - Initial contribution
*/
@NonNullByDefault
public class NanoleafUnauthorizedException extends NanoleafException {
private static final long serialVersionUID = -6941678941424573257L;
public NanoleafUnauthorizedException(String message) {
super(message);
}
}

View File

@@ -0,0 +1,169 @@
/**
* Copyright (c) 2010-2020 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;
import static org.openhab.binding.nanoleaf.internal.NanoleafBindingConstants.*;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.Iterator;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeoutException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.HttpResponseException;
import org.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.HttpScheme;
import org.eclipse.jetty.http.HttpStatus;
import org.openhab.binding.nanoleaf.internal.config.NanoleafControllerConfig;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link OpenAPIUtils} offers helper methods to support API communication with the controller
*
* @author Martin Raepple - Initial contribution
*/
@NonNullByDefault
public class OpenAPIUtils {
private static final Logger LOGGER = LoggerFactory.getLogger(OpenAPIUtils.class);
// Regular expression for firmware version
private static final Pattern FIRMWARE_VERSION_PATTERN = Pattern.compile("(\\d+)\\.(\\d+)\\.(\\d+)");
public static Request requestBuilder(HttpClient httpClient, NanoleafControllerConfig controllerConfig,
String apiOperation, HttpMethod method) throws NanoleafException {
URI requestURI = getUri(controllerConfig, apiOperation, null);
LOGGER.trace("RequestBuilder: Sending Request {}:{} {} ", requestURI.getHost(), requestURI.getPort(),
requestURI.getPath());
return httpClient.newRequest(requestURI).method(method);
}
public static URI getUri(NanoleafControllerConfig controllerConfig, String apiOperation, @Nullable String query)
throws NanoleafException {
String path;
// get network settings from configuration
String address = controllerConfig.address;
int port = controllerConfig.port;
if (API_ADD_USER.equals(apiOperation)) {
path = String.format("%s%s", API_V1_BASE_URL, apiOperation);
} else {
String authToken = controllerConfig.authToken;
if (authToken != null) {
path = String.format("%s/%s%s", API_V1_BASE_URL, authToken, apiOperation);
} else {
throw new NanoleafUnauthorizedException("No authentication token found in configuration");
}
}
URI requestURI;
try {
requestURI = new URI(HttpScheme.HTTP.asString(), null, address, port, path, query, null);
} catch (URISyntaxException use) {
LOGGER.warn("URI could not be parsed with path {}", path);
throw new NanoleafException("Wrong URI format for API request");
}
return requestURI;
}
public static ContentResponse sendOpenAPIRequest(Request request) throws NanoleafException {
try {
traceSendRequest(request);
ContentResponse openAPIResponse;
openAPIResponse = request.send();
if (LOGGER.isTraceEnabled()) {
LOGGER.trace("API response from Nanoleaf controller: {}", openAPIResponse.getContentAsString());
}
LOGGER.debug("API response code: {}", openAPIResponse.getStatus());
int responseStatus = openAPIResponse.getStatus();
if (responseStatus == HttpStatus.OK_200 || responseStatus == HttpStatus.NO_CONTENT_204) {
return openAPIResponse;
} else {
if (openAPIResponse.getStatus() == HttpStatus.UNAUTHORIZED_401) {
throw new NanoleafUnauthorizedException("OpenAPI request unauthorized");
} else if (openAPIResponse.getStatus() == HttpStatus.NOT_FOUND_404) {
throw new NanoleafNotFoundException("OpenAPI request did not get any result back");
} else if (openAPIResponse.getStatus() == HttpStatus.BAD_REQUEST_400) {
throw new NanoleafBadRequestException(
String.format("Nanoleaf did not expect this request. HTTP response code %s",
openAPIResponse.getStatus()));
} else {
throw new NanoleafException(String.format("OpenAPI request failed. HTTP response code %s",
openAPIResponse.getStatus()));
}
}
} catch (ExecutionException | TimeoutException clientException) {
if (clientException.getCause() instanceof HttpResponseException
&& ((HttpResponseException) clientException.getCause()).getResponse()
.getStatus() == HttpStatus.UNAUTHORIZED_401) {
LOGGER.warn("OpenAPI request unauthorized. Invalid authorization token.");
throw new NanoleafUnauthorizedException("Invalid authorization token");
}
throw new NanoleafException("Failed to send OpenAPI request", clientException);
} catch (InterruptedException interruptedException) {
throw new NanoleafInterruptedException("OpenAPI request has been interrupted", interruptedException);
}
}
private static void traceSendRequest(Request request) {
if (!LOGGER.isTraceEnabled()) {
return;
}
LOGGER.trace("Sending Request {} {}", request.getURI(),
request.getQuery() == null ? "no query parameters" : request.getQuery());
LOGGER.trace("Request method:{} uri:{} params{}\n", request.getMethod(), request.getURI(), request.getParams());
if (request.getContent() != null) {
Iterator<ByteBuffer> iter = request.getContent().iterator();
if (iter != null) {
while (iter.hasNext()) {
@Nullable
ByteBuffer buffer = iter.next();
LOGGER.trace("Content {}", StandardCharsets.UTF_8.decode(buffer).toString());
}
}
}
}
public static boolean checkRequiredFirmware(String modelId, String currentFirmwareVersion) {
int[] currentVer = getFirmwareVersionNumbers(currentFirmwareVersion);
int[] requiredVer = getFirmwareVersionNumbers(
MODEL_ID_LIGHTPANELS.equals(modelId) ? API_MIN_FW_VER_LIGHTPANELS : API_MIN_FW_VER_CANVAS);
for (int i = 0; i < currentVer.length; i++) {
if (currentVer[i] != requiredVer[i]) {
return currentVer[i] > requiredVer[i];
}
}
return true;
}
private static int[] getFirmwareVersionNumbers(String firmwareVersion) throws IllegalArgumentException {
Matcher m = FIRMWARE_VERSION_PATTERN.matcher(firmwareVersion);
if (!m.matches()) {
throw new IllegalArgumentException("Malformed controller firmware version");
}
return new int[] { Integer.parseInt(m.group(1)), Integer.parseInt(m.group(2)), Integer.parseInt(m.group(3)) };
}
}

View File

@@ -0,0 +1,41 @@
/**
* Copyright (c) 2010-2020 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.config;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* The {@link NanoleafControllerConfig} class contains fields mapping controller configuration parameters.
*
* @author Martin Raepple - Initial contribution
*/
@NonNullByDefault
public class NanoleafControllerConfig {
/** IP address or hostname of the light panels controller */
public static final String ADDRESS = "address";
public String address = "";
/** Port number of the light panels controller */
public static final String PORT = "port";
public int port = 16021;
/** Authorization token for controller API */
public static final String AUTH_TOKEN = "authToken";
public @Nullable String authToken;
/** Light panels status refresh interval */
public static final String REFRESH_INTERVAL = "refreshInterval";
public int refreshInterval = 60;
/** Nanoleaf device type: Light panels or Canvas */
public static final String DEVICE_TYPE = "deviceType";
public String deviceType = "lightPanels";
}

View File

@@ -0,0 +1,36 @@
/**
* Copyright (c) 2010-2020 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.config;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link NanoleafPanelConfig} class contains fields mapping an individual panel configuration parameters.
*
* @author Martin Raepple - Initial contribution
*/
@NonNullByDefault
public class NanoleafPanelConfig {
/** ID of the light panel assigned by the controller */
public static final String ID = "id";
public Integer id = 0;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
}

View File

@@ -0,0 +1,114 @@
/**
* Copyright (c) 2010-2020 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.discovery;
import static org.openhab.binding.nanoleaf.internal.NanoleafBindingConstants.*;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import javax.jmdns.ServiceInfo;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.nanoleaf.internal.NanoleafHandlerFactory;
import org.openhab.binding.nanoleaf.internal.OpenAPIUtils;
import org.openhab.core.config.discovery.DiscoveryResult;
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
import org.openhab.core.config.discovery.mdns.MDNSDiscoveryParticipant;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.ThingUID;
import org.osgi.service.component.annotations.Component;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link NanoleafMDNSDiscoveryParticipant} is responsible for discovering new Nanoleaf controllers (bridges).
*
* @author Martin Raepple - Initial contribution
* @author Stefan Höhn - further improvements for static defined things
* @see <a href="https://openhab.org/documentation/development/bindings/discovery-services.html">MSDN
* Discovery</a>
*/
@Component(immediate = true, configurationPid = "discovery.nanoleaf")
@NonNullByDefault
public class NanoleafMDNSDiscoveryParticipant implements MDNSDiscoveryParticipant {
private final Logger logger = LoggerFactory.getLogger(NanoleafMDNSDiscoveryParticipant.class);
@Override
public Set<ThingTypeUID> getSupportedThingTypeUIDs() {
return NanoleafHandlerFactory.SUPPORTED_THING_TYPES_UIDS;
}
@Override
public String getServiceType() {
return SERVICE_TYPE;
}
@Override
public @Nullable DiscoveryResult createResult(ServiceInfo service) {
final ThingUID uid = getThingUID(service);
if (uid == null) {
return null;
}
final Map<String, Object> properties = new HashMap<>(2);
String host = service.getHostAddresses()[0];
properties.put(CONFIG_ADDRESS, host);
int port = service.getPort();
properties.put(CONFIG_PORT, port);
String firmwareVersion = service.getPropertyString("srcvers");
properties.put(Thing.PROPERTY_FIRMWARE_VERSION, firmwareVersion);
String modelId = service.getPropertyString("md");
properties.put(Thing.PROPERTY_MODEL_ID, modelId);
properties.put(Thing.PROPERTY_VENDOR, "Nanoleaf");
String qualifiedName = service.getQualifiedName();
logger.debug("AVR found: {}", qualifiedName);
logger.trace("Discovered nanoleaf host: {} port: {} firmWare: {} modelId: {} qualifiedName: {}", host, port,
firmwareVersion, modelId, qualifiedName);
logger.debug("Adding Nanoleaf controller {} with FW version {} found at {} {} to inbox", qualifiedName,
firmwareVersion, host, port);
if (!OpenAPIUtils.checkRequiredFirmware(service.getPropertyString("md"), firmwareVersion)) {
logger.warn("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);
}
final DiscoveryResult result = DiscoveryResultBuilder.create(uid).withThingType(getThingType(service))
.withProperties(properties).withLabel(service.getName()).withRepresentationProperty(CONFIG_ADDRESS)
.build();
return result;
}
@Override
public @Nullable ThingUID getThingUID(ServiceInfo service) {
ThingTypeUID thingTypeUID = getThingType(service);
if (thingTypeUID != null) {
String id = service.getPropertyString("id").replace(":", "");
return new ThingUID(thingTypeUID, id);
} else {
return null;
}
}
private @Nullable ThingTypeUID getThingType(final ServiceInfo service) {
String model = service.getPropertyString("md"); // model
logger.debug("Nanoleaf Type: {}", model);
if (model == null) {
return null;
}
return THING_TYPE_CONTROLLER;
}
}

View File

@@ -0,0 +1,118 @@
/**
* Copyright (c) 2010-2020 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.discovery;
import static org.openhab.binding.nanoleaf.internal.NanoleafBindingConstants.CONFIG_PANEL_ID;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
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.NanoleafHandlerFactory;
import org.openhab.binding.nanoleaf.internal.handler.NanoleafControllerHandler;
import org.openhab.binding.nanoleaf.internal.model.ControllerInfo;
import org.openhab.binding.nanoleaf.internal.model.Layout;
import org.openhab.binding.nanoleaf.internal.model.PanelLayout;
import org.openhab.binding.nanoleaf.internal.model.PositionDatum;
import org.openhab.core.config.discovery.AbstractDiscoveryService;
import org.openhab.core.config.discovery.DiscoveryResult;
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
import org.openhab.core.thing.ThingUID;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link NanoleafPanelsDiscoveryService} is responsible for discovering the individual
* panels connected to the controller.
*
* @author Martin Raepple - Initial contribution
*/
@NonNullByDefault
public class NanoleafPanelsDiscoveryService extends AbstractDiscoveryService implements NanoleafControllerListener {
private static final int SEARCH_TIMEOUT_SECONDS = 60;
private final Logger logger = LoggerFactory.getLogger(NanoleafPanelsDiscoveryService.class);
private final NanoleafControllerHandler bridgeHandler;
/**
* Constructs a new {@link NanoleafPanelsDiscoveryService} attached to the given bridge handler.
*
* @param nanoleafControllerHandler The bridge handler this discovery service is attached to
*/
public NanoleafPanelsDiscoveryService(NanoleafControllerHandler nanoleafControllerHandler) {
super(NanoleafHandlerFactory.SUPPORTED_THING_TYPES_UIDS, SEARCH_TIMEOUT_SECONDS, false);
this.bridgeHandler = nanoleafControllerHandler;
}
@Override
protected void startScan() {
logger.debug("Starting Nanoleaf panel discovery");
bridgeHandler.registerControllerListener(this);
}
@Override
protected synchronized void stopScan() {
logger.debug("Stopping Nanoleaf panel discovery");
super.stopScan();
bridgeHandler.unregisterControllerListener(this);
}
/**
* Called by the controller handler with bridge and panel data
*
* @param bridge The controller
* @param controllerInfo Panel data (and more)
*/
@Override
public void onControllerInfoFetched(ThingUID bridge, ControllerInfo controllerInfo) {
logger.debug("Discover panels connected to controller with id {}", bridge.getAsString());
final PanelLayout panelLayout = controllerInfo.getPanelLayout();
@Nullable
Layout layout = panelLayout.getLayout();
if (layout != null && layout.getNumPanels() > 0) {
@Nullable
final List<PositionDatum> positionData = layout.getPositionData();
if (positionData != null) {
Iterator<PositionDatum> iterator = positionData.iterator();
while (iterator.hasNext()) {
@Nullable
PositionDatum panel = iterator.next();
ThingUID newPanelThingUID = new ThingUID(NanoleafBindingConstants.THING_TYPE_LIGHT_PANEL, bridge,
Integer.toString(panel.getPanelId()));
final Map<String, Object> properties = new HashMap<>(1);
properties.put(CONFIG_PANEL_ID, panel.getPanelId());
DiscoveryResult newPanel = DiscoveryResultBuilder.create(newPanelThingUID).withBridge(bridge)
.withProperties(properties).withLabel("Light Panel " + panel.getPanelId())
.withRepresentationProperty(CONFIG_PANEL_ID).build();
logger.debug("Adding panel with id {} to inbox", panel.getPanelId());
thingDiscovered(newPanel);
}
} else {
logger.debug("Couldn't add panels to inbox as layout position data was null");
}
} else {
logger.info("No panels found or connected to controller");
}
}
}

View File

@@ -0,0 +1,855 @@
/**
* Copyright (c) 2010-2020 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.handler;
import static org.openhab.binding.nanoleaf.internal.NanoleafBindingConstants.*;
import java.net.URI;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Map;
import java.util.Scanner;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import org.apache.commons.lang.StringUtils;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.client.api.Response;
import org.eclipse.jetty.client.api.Result;
import org.eclipse.jetty.client.util.StringContentProvider;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.HttpStatus;
import org.openhab.binding.nanoleaf.internal.*;
import org.openhab.binding.nanoleaf.internal.config.NanoleafControllerConfig;
import org.openhab.binding.nanoleaf.internal.model.*;
import org.openhab.core.config.core.Configuration;
import org.openhab.core.library.types.DecimalType;
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.library.types.StringType;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.binding.BaseBridgeHandler;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
import com.google.gson.JsonSyntaxException;
/**
* The {@link NanoleafControllerHandler} is responsible for handling commands to the controller which
* affect all panels connected to it (e.g. selected effect)
*
* @author Martin Raepple - Initial contribution
* @author Stefan Höhn - Canvas Touch Support
*/
@NonNullByDefault
public class NanoleafControllerHandler extends BaseBridgeHandler {
// Pairing interval in seconds
private static final int PAIRING_INTERVAL = 25;
// Panel discovery interval in seconds
private static final int PANEL_DISCOVERY_INTERVAL = 30;
private final Logger logger = LoggerFactory.getLogger(NanoleafControllerHandler.class);
private HttpClient httpClient;
private List<NanoleafControllerListener> controllerListeners = new CopyOnWriteArrayList<>();
// Pairing, update and panel discovery jobs and touch event job
private @NonNullByDefault({}) ScheduledFuture<?> pairingJob;
private @NonNullByDefault({}) ScheduledFuture<?> updateJob;
private @NonNullByDefault({}) ScheduledFuture<?> panelDiscoveryJob;
private @NonNullByDefault({}) ScheduledFuture<?> touchJob;
// JSON parser for API responses
private final Gson gson = new Gson();
// Controller configuration settings and channel values
private @Nullable String address;
private int port;
private int refreshIntervall;
private @Nullable String authToken;
private @Nullable String deviceType;
private @NonNullByDefault({}) ControllerInfo controllerInfo;
public NanoleafControllerHandler(Bridge bridge, HttpClient httpClient) {
super(bridge);
this.httpClient = httpClient;
}
@Override
public void initialize() {
logger.debug("Initializing the controller (bridge)");
updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.BRIDGE_UNINITIALIZED);
NanoleafControllerConfig config = getConfigAs(NanoleafControllerConfig.class);
setAddress(config.address);
setPort(config.port);
setRefreshIntervall(config.refreshInterval);
setAuthToken(config.authToken);
@Nullable
String property = getThing().getProperties().get(Thing.PROPERTY_MODEL_ID);
if (MODEL_ID_CANVAS.equals(property)) {
config.deviceType = DEVICE_TYPE_CANVAS;
} else {
config.deviceType = DEVICE_TYPE_LIGHTPANELS;
}
setDeviceType(config.deviceType);
try {
if (StringUtils.isEmpty(getAddress()) || StringUtils.isEmpty(String.valueOf(getPort()))) {
logger.warn("No IP address and port configured for the Nanoleaf controller");
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING,
"@text/error.nanoleaf.controller.noIp");
stopAllJobs();
} else if (!StringUtils.isEmpty(getThing().getProperties().get(Thing.PROPERTY_FIRMWARE_VERSION))
&& !OpenAPIUtils.checkRequiredFirmware(getThing().getProperties().get(Thing.PROPERTY_MODEL_ID),
getThing().getProperties().get(Thing.PROPERTY_FIRMWARE_VERSION))) {
logger.warn("Nanoleaf controller firmware is too old: {}. Must be equal or higher than {}",
getThing().getProperties().get(Thing.PROPERTY_FIRMWARE_VERSION), API_MIN_FW_VER_LIGHTPANELS);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"@text/error.nanoleaf.controller.incompatibleFirmware");
stopAllJobs();
} else if (StringUtils.isEmpty(getAuthToken())) {
logger.debug("No token found. Start pairing background job");
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING,
"@text/error.nanoleaf.controller.noToken");
startPairingJob();
stopUpdateJob();
stopPanelDiscoveryJob();
} else {
logger.debug("Controller is online. Stop pairing job, start update & panel discovery jobs");
updateStatus(ThingStatus.ONLINE);
stopPairingJob();
startUpdateJob();
startPanelDiscoveryJob();
startTouchJob();
}
} catch (IllegalArgumentException iae) {
logger.warn("Nanoleaf controller firmware version not in format x.y.z: {}",
getThing().getProperties().get(Thing.PROPERTY_FIRMWARE_VERSION));
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"@text/error.nanoleaf.controller.incompatibleFirmware");
}
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
logger.debug("Received command {} for channel {}", command, channelUID);
if (!ThingStatus.ONLINE.equals(getThing().getStatusInfo().getStatus())) {
logger.debug("Cannot handle command. Bridge is not online.");
return;
}
try {
if (command instanceof RefreshType) {
updateFromControllerInfo();
} else {
switch (channelUID.getId()) {
case CHANNEL_POWER:
case CHANNEL_COLOR:
case CHANNEL_COLOR_TEMPERATURE:
case CHANNEL_COLOR_TEMPERATURE_ABS:
case CHANNEL_PANEL_LAYOUT:
sendStateCommand(channelUID.getId(), command);
break;
case CHANNEL_EFFECT:
sendEffectCommand(command);
break;
case CHANNEL_RHYTHM_MODE:
sendRhythmCommand(command);
break;
default:
logger.warn("Channel with id {} not handled", channelUID.getId());
break;
}
}
} catch (NanoleafUnauthorizedException nae) {
logger.warn("Authorization for command {} to channelUID {} failed: {}", command, channelUID,
nae.getMessage());
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"@text/error.nanoleaf.controller.invalidToken");
} catch (NanoleafException ne) {
logger.warn("Handling command {} to channelUID {} failed: {}", command, channelUID, ne.getMessage());
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"@text/error.nanoleaf.controller.communication");
}
}
@Override
public void handleRemoval() {
// delete token for openHAB
ContentResponse deleteTokenResponse;
try {
Request deleteTokenRequest = OpenAPIUtils.requestBuilder(httpClient, getControllerConfig(), API_DELETE_USER,
HttpMethod.DELETE);
deleteTokenResponse = OpenAPIUtils.sendOpenAPIRequest(deleteTokenRequest);
if (deleteTokenResponse.getStatus() != HttpStatus.NO_CONTENT_204) {
logger.warn("Failed to delete token for openHAB. Response code is {}", deleteTokenResponse.getStatus());
return;
}
logger.debug("Successfully deleted token for openHAB from controller");
} catch (NanoleafUnauthorizedException e) {
logger.warn("Attempt to delete token for openHAB failed. Token unauthorized.");
} catch (NanoleafException ne) {
logger.warn("Attempt to delete token for openHAB failed : {}", ne.getMessage());
}
stopAllJobs();
super.handleRemoval();
logger.debug("Nanoleaf controller removed");
}
@Override
public void dispose() {
stopAllJobs();
super.dispose();
logger.debug("Disposing handler for Nanoleaf controller {}", getThing().getUID());
}
public boolean registerControllerListener(NanoleafControllerListener controllerListener) {
logger.debug("Register new listener for controller {}", getThing().getUID());
boolean result = controllerListeners.add(controllerListener);
if (result) {
startPanelDiscoveryJob();
}
return result;
}
public boolean unregisterControllerListener(NanoleafControllerListener controllerListener) {
logger.debug("Unregister listener for controller {}", getThing().getUID());
boolean result = controllerListeners.remove(controllerListener);
if (result) {
stopPanelDiscoveryJob();
}
return result;
}
public NanoleafControllerConfig getControllerConfig() {
NanoleafControllerConfig config = new NanoleafControllerConfig();
config.address = this.getAddress();
config.port = this.getPort();
config.refreshInterval = this.getRefreshIntervall();
config.authToken = this.getAuthToken();
config.deviceType = this.getDeviceType();
return config;
}
public synchronized void startPairingJob() {
if (pairingJob == null || pairingJob.isCancelled()) {
logger.debug("Start pairing job, interval={} sec", PAIRING_INTERVAL);
pairingJob = scheduler.scheduleWithFixedDelay(this::runPairing, 0, PAIRING_INTERVAL, TimeUnit.SECONDS);
}
}
private synchronized void stopPairingJob() {
if (pairingJob != null && !pairingJob.isCancelled()) {
logger.debug("Stop pairing job");
pairingJob.cancel(true);
this.pairingJob = null;
}
}
private synchronized void startUpdateJob() {
if (StringUtils.isNotEmpty(getAuthToken())) {
if (updateJob == null || updateJob.isCancelled()) {
logger.debug("Start controller status job, repeat every {} sec", getRefreshIntervall());
updateJob = scheduler.scheduleWithFixedDelay(this::runUpdate, 0, getRefreshIntervall(),
TimeUnit.SECONDS);
}
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING,
"@text/error.nanoleaf.controller.noToken");
}
}
private synchronized void stopUpdateJob() {
if (updateJob != null && !updateJob.isCancelled()) {
logger.debug("Stop status job");
updateJob.cancel(true);
this.updateJob = null;
}
}
public synchronized void startPanelDiscoveryJob() {
logger.debug("Starting panel discovery job. Has Controller-Listeners: {} panelDiscoveryJob: {}",
!controllerListeners.isEmpty(), panelDiscoveryJob);
if (!controllerListeners.isEmpty() && (panelDiscoveryJob == null || panelDiscoveryJob.isCancelled())) {
logger.debug("Start panel discovery job, interval={} sec", PANEL_DISCOVERY_INTERVAL);
panelDiscoveryJob = scheduler.scheduleWithFixedDelay(this::runPanelDiscovery, 0, PANEL_DISCOVERY_INTERVAL,
TimeUnit.SECONDS);
}
}
private synchronized void stopPanelDiscoveryJob() {
if (controllerListeners.isEmpty() && panelDiscoveryJob != null && !panelDiscoveryJob.isCancelled()) {
logger.debug("Stop panel discovery job");
panelDiscoveryJob.cancel(true);
this.panelDiscoveryJob = null;
}
}
private synchronized void startTouchJob() {
NanoleafControllerConfig config = getConfigAs(NanoleafControllerConfig.class);
if (!config.deviceType.equals(DEVICE_TYPE_CANVAS)) {
logger.debug("NOT starting TouchJob for Panel {} because it has wrong device type '{}' vs required '{}'",
this.getThing().getUID(), config.deviceType, DEVICE_TYPE_CANVAS);
return;
} else
logger.debug("Starting TouchJob for Panel {}", this.getThing().getUID());
if (StringUtils.isNotEmpty(getAuthToken())) {
if (touchJob == null || touchJob.isCancelled()) {
logger.debug("Starting Touchjob now");
touchJob = scheduler.schedule(this::runTouchDetection, 0, TimeUnit.SECONDS);
}
} else {
logger.error("starting TouchJob for Controller {} failed - missing token", this.getThing().getUID());
}
}
private synchronized void stopTouchJob() {
if (touchJob != null && !touchJob.isCancelled()) {
logger.debug("Stop touch job");
touchJob.cancel(true);
this.touchJob = null;
}
}
private void runUpdate() {
logger.debug("Run update job");
try {
updateFromControllerInfo();
startTouchJob(); // if device type has changed, start touch detection.
// controller might have been offline, e.g. for firmware update. In this case, return to online state
if (ThingStatus.OFFLINE.equals(getThing().getStatus())) {
logger.debug("Controller {} is back online", thing.getUID());
updateStatus(ThingStatus.ONLINE);
}
} catch (NanoleafUnauthorizedException nae) {
logger.warn("Status update unauthorized: {}", nae.getMessage());
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"@text/error.nanoleaf.controller.invalidToken");
if (StringUtils.isEmpty(getAuthToken())) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING,
"@text/error.nanoleaf.controller.noToken");
}
} catch (NanoleafException ne) {
logger.warn("Status update failed: {}", ne.getMessage());
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"@text/error.nanoleaf.controller.communication");
} catch (RuntimeException e) {
logger.warn("Update job failed", e);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "@text/error.nanoleaf.controller.runtime");
}
}
private void runPairing() {
logger.debug("Run pairing job");
try {
if (StringUtils.isNotEmpty(getAuthToken())) {
if (pairingJob != null) {
pairingJob.cancel(false);
}
logger.debug("Authentication token found. Canceling pairing job");
return;
}
ContentResponse authTokenResponse = OpenAPIUtils
.requestBuilder(httpClient, getControllerConfig(), API_ADD_USER, HttpMethod.POST).send();
if (logger.isTraceEnabled()) {
logger.trace("Auth token response: {}", authTokenResponse.getContentAsString());
}
if (authTokenResponse.getStatus() != HttpStatus.OK_200) {
logger.debug("Pairing pending for {}. Controller returns status code {}", this.getThing().getUID(),
authTokenResponse.getStatus());
} else {
// get auth token from response
@Nullable
AuthToken authToken = gson.fromJson(authTokenResponse.getContentAsString(), AuthToken.class);
if (StringUtils.isNotEmpty(authToken.getAuthToken())) {
logger.debug("Pairing succeeded.");
// Update and save the auth token in the thing configuration
Configuration config = editConfiguration();
config.put(NanoleafControllerConfig.AUTH_TOKEN, authToken.getAuthToken());
updateConfiguration(config);
updateStatus(ThingStatus.ONLINE);
// Update local field
setAuthToken(authToken.getAuthToken());
stopPairingJob();
startUpdateJob();
startPanelDiscoveryJob();
startTouchJob();
} else {
logger.debug("No auth token found in response: {}", authTokenResponse.getContentAsString());
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"@text/error.nanoleaf.controller.pairingFailed");
throw new NanoleafException(authTokenResponse.getContentAsString());
}
}
} catch (JsonSyntaxException e) {
logger.warn("Received invalid data", e);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"@text/error.nanoleaf.controller.invalidData");
} catch (NanoleafException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"@text/error.nanoleaf.controller.noTokenReceived");
} catch (InterruptedException | ExecutionException | TimeoutException e) {
logger.warn("Cannot send authorization request to controller: ", e);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"@text/error.nanoleaf.controller.authRequest");
} catch (RuntimeException e) {
logger.warn("Pairing job failed", e);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "@text/error.nanoleaf.controller.runtime");
} catch (Exception e) {
logger.warn("Cannot start http client", e);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"@text/error.nanoleaf.controller.noClient");
}
}
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");
if (StringUtils.isEmpty(getAuthToken())) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING,
"@text/error.nanoleaf.controller.noToken");
}
} catch (NanoleafInterruptedException nie) {
logger.info("Panel discovery has been stopped.");
} catch (NanoleafException ne) {
logger.warn("Failed to discover panels: ", ne);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"@text/error.nanoleaf.controller.communication");
} catch (RuntimeException e) {
logger.warn("Panel discovery job failed", e);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "@text/error.nanoleaf.controller.runtime");
}
}
}
/**
* This is based on the touch event detection described in https://forum.nanoleaf.me/docs/openapi#_842h3097vbgq
*/
private static boolean touchJobRunning = false;
private void runTouchDetection() {
if (touchJobRunning) {
logger.debug("touch job already running. quitting.");
return;
}
try {
touchJobRunning = true;
URI eventUri = OpenAPIUtils.getUri(getControllerConfig(), API_EVENTS, "id=4");
logger.debug("touch job registered on: {}", eventUri.toString());
httpClient.newRequest(eventUri).send(new Response.Listener.Adapter() // request runs forever
{
@Override
public void onContent(@Nullable Response response, @Nullable ByteBuffer content) {
String s = StandardCharsets.UTF_8.decode(content).toString();
logger.trace("content {}", s);
Scanner eventContent = new Scanner(s);
while (eventContent.hasNextLine()) {
String line = eventContent.nextLine().trim();
// we don't expect anything than content id:4, so we do not check that but only care about the
// data part
if (line.startsWith("data:")) {
String json = line.substring(5).trim(); // supposed to be JSON
try {
@Nullable
TouchEvents touchEvents = gson.fromJson(json, TouchEvents.class);
handleTouchEvents(touchEvents);
} catch (JsonSyntaxException jse) {
logger.error("couldn't parse touch event json {}", json);
}
}
}
eventContent.close();
logger.debug("leaving touch onContent");
super.onContent(response, content);
}
@Override
public void onSuccess(@Nullable Response response) {
logger.trace("touch event SUCCESS: {}", response);
}
@Override
public void onFailure(@Nullable Response response, @Nullable Throwable failure) {
logger.trace("touch event FAILURE: {}", response);
}
@Override
public void onComplete(@Nullable Result result) {
logger.trace("touch event COMPLETE: {}", result);
}
});
} catch (RuntimeException | NanoleafException e) {
logger.warn("setting up TouchDetection failed", e);
} finally {
touchJobRunning = false;
}
logger.debug("leaving run touch detection");
}
/**
* Interate over all gathered touch events and apply them to the panel they belong to
*
* @param touchEvents
*/
private void handleTouchEvents(TouchEvents touchEvents) {
touchEvents.getEvents().forEach(event -> {
logger.info("panel: {} gesture id: {}", event.getPanelId(), event.getGesture());
// Iterate over all child things = all panels of that controller
this.getThing().getThings().forEach(child -> {
NanoleafPanelHandler panelHandler = (NanoleafPanelHandler) child.getHandler();
if (panelHandler != null) {
logger.trace("Checking available panel -{}- versus event panel -{}-", panelHandler.getPanelID(),
event.getPanelId());
if (panelHandler.getPanelID().equals(event.getPanelId())) {
logger.debug("Panel {} found. Triggering item with gesture {}.", panelHandler.getPanelID(),
event.getGesture());
panelHandler.updatePanelGesture(event.getGesture());
}
}
});
});
}
private void updateFromControllerInfo() throws NanoleafException {
logger.debug("Update channels for controller {}", thing.getUID());
this.controllerInfo = receiveControllerInfo();
if (controllerInfo == null) {
logger.debug("No Controller Info has been provided");
return;
}
final State state = controllerInfo.getState();
OnOffType powerState = state.getOnOff();
updateState(CHANNEL_POWER, powerState);
@Nullable
Ct colorTemperature = state.getColorTemperature();
float colorTempPercent = 0f;
if (colorTemperature != null) {
updateState(CHANNEL_COLOR_TEMPERATURE_ABS, new DecimalType(colorTemperature.getValue()));
@Nullable
Integer min = colorTemperature.getMin();
int colorMin = (min == null) ? 0 : min;
@Nullable
Integer max = colorTemperature.getMax();
int colorMax = (max == null) ? 0 : max;
colorTempPercent = (colorTemperature.getValue() - colorMin) / (colorMax - colorMin)
* PercentType.HUNDRED.intValue();
}
updateState(CHANNEL_COLOR_TEMPERATURE, new PercentType(Float.toString(colorTempPercent)));
updateState(CHANNEL_EFFECT, new StringType(controllerInfo.getEffects().getSelect()));
@Nullable
Hue stateHue = state.getHue();
int hue = (stateHue != null) ? stateHue.getValue() : 0;
@Nullable
Sat stateSaturation = state.getSaturation();
int saturation = (stateSaturation != null) ? stateSaturation.getValue() : 0;
@Nullable
Brightness stateBrightness = state.getBrightness();
int brightness = (stateBrightness != null) ? stateBrightness.getValue() : 0;
updateState(CHANNEL_COLOR, new HSBType(new DecimalType(hue), new PercentType(saturation),
new PercentType(powerState == OnOffType.ON ? brightness : 0)));
updateState(CHANNEL_COLOR_MODE, new StringType(state.getColorMode()));
updateState(CHANNEL_RHYTHM_ACTIVE, controllerInfo.getRhythm().getRhythmActive() ? OnOffType.ON : OnOffType.OFF);
updateState(CHANNEL_RHYTHM_MODE, new DecimalType(controllerInfo.getRhythm().getRhythmMode()));
updateState(CHANNEL_RHYTHM_STATE,
controllerInfo.getRhythm().getRhythmConnected() ? OnOffType.ON : OnOffType.OFF);
// update bridge properties which may have changed, or are not present during discovery
Map<String, String> properties = editProperties();
properties.put(Thing.PROPERTY_SERIAL_NUMBER, controllerInfo.getSerialNo());
properties.put(Thing.PROPERTY_FIRMWARE_VERSION, controllerInfo.getFirmwareVersion());
properties.put(Thing.PROPERTY_MODEL_ID, controllerInfo.getModel());
properties.put(Thing.PROPERTY_VENDOR, controllerInfo.getManufacturer());
updateProperties(properties);
Configuration config = editConfiguration();
if (MODEL_ID_CANVAS.equals(controllerInfo.getModel())) {
config.put(NanoleafControllerConfig.DEVICE_TYPE, DEVICE_TYPE_CANVAS);
logger.debug("Set to device type {}", DEVICE_TYPE_CANVAS);
} else {
config.put(NanoleafControllerConfig.DEVICE_TYPE, DEVICE_TYPE_LIGHTPANELS);
logger.debug("Set to device type {}", DEVICE_TYPE_LIGHTPANELS);
}
updateConfiguration(config);
getConfig().getProperties().forEach((key, value) -> {
logger.trace("Configuration property: key {} value {}", key, value);
});
getThing().getProperties().forEach((key, value) -> {
logger.debug("Thing property: key {} value {}", key, value);
});
// update the color channels of each panel
this.getThing().getThings().forEach(child -> {
NanoleafPanelHandler panelHandler = (NanoleafPanelHandler) child.getHandler();
if (panelHandler != null) {
logger.debug("Update color channel for panel {}", panelHandler.getThing().getUID());
panelHandler.updatePanelColorChannel();
}
});
}
private ControllerInfo receiveControllerInfo() throws NanoleafException, NanoleafUnauthorizedException {
ContentResponse controllerlInfoJSON = OpenAPIUtils.sendOpenAPIRequest(OpenAPIUtils.requestBuilder(httpClient,
getControllerConfig(), API_GET_CONTROLLER_INFO, HttpMethod.GET));
@Nullable
ControllerInfo controllerInfo = gson.fromJson(controllerlInfoJSON.getContentAsString(), ControllerInfo.class);
return controllerInfo;
}
private void sendStateCommand(String channel, Command command) throws NanoleafException {
State stateObject = new State();
switch (channel) {
case CHANNEL_POWER:
if (command instanceof OnOffType) {
// On/Off command - turns controller on/off
BooleanState state = new On();
state.setValue(OnOffType.ON.equals(command));
stateObject.setState(state);
} else {
logger.warn("Unhandled command type: {}", command.getClass().getName());
return;
}
break;
case CHANNEL_COLOR:
if (command instanceof OnOffType) {
// On/Off command - turns controller on/off
BooleanState state = new On();
state.setValue(OnOffType.ON.equals(command));
stateObject.setState(state);
} else if (command instanceof HSBType) {
// regular color HSB command
IntegerState h = new Hue();
IntegerState s = new Sat();
IntegerState b = new Brightness();
h.setValue(((HSBType) command).getHue().intValue());
s.setValue(((HSBType) command).getSaturation().intValue());
b.setValue(((HSBType) command).getBrightness().intValue());
stateObject.setState(h);
stateObject.setState(s);
stateObject.setState(b);
} else if (command instanceof PercentType) {
// brightness command
IntegerState b = new Brightness();
b.setValue(((PercentType) command).intValue());
stateObject.setState(b);
} else if (command instanceof IncreaseDecreaseType) {
// increase/decrease brightness
if (controllerInfo != null) {
@Nullable
Brightness brightness = controllerInfo.getState().getBrightness();
int brightnessMin = 0;
int brightnessMax = 0;
if (brightness != null) {
@Nullable
Integer min = brightness.getMin();
brightnessMin = (min == null) ? 0 : min;
@Nullable
Integer max = brightness.getMax();
brightnessMax = (max == null) ? 0 : max;
if (IncreaseDecreaseType.INCREASE.equals(command)) {
brightness.setValue(
Math.min(brightnessMax, brightness.getValue() + BRIGHTNESS_STEP_SIZE));
} else {
brightness.setValue(
Math.max(brightnessMin, brightness.getValue() - BRIGHTNESS_STEP_SIZE));
}
stateObject.setState(brightness);
logger.debug("Setting controller brightness to {}", brightness.getValue());
// update controller info in case new command is sent before next update job interval
controllerInfo.getState().setBrightness(brightness);
} else {
logger.debug("Couldn't set brightness as it was null!");
}
}
} else {
logger.warn("Unhandled command type: {}", command.getClass().getName());
return;
}
break;
case CHANNEL_COLOR_TEMPERATURE:
if (command instanceof PercentType) {
// Color temperature (percent)
IntegerState state = new Ct();
@Nullable
Ct colorTemperature = controllerInfo.getState().getColorTemperature();
int colorMin = 0;
int colorMax = 0;
if (colorTemperature != null) {
@Nullable
Integer min = colorTemperature.getMin();
colorMin = (min == null) ? 0 : min;
@Nullable
Integer max = colorTemperature.getMax();
colorMax = (max == null) ? 0 : max;
}
state.setValue(Math.round((colorMax - colorMin) * ((PercentType) command).intValue()
/ PercentType.HUNDRED.floatValue() + colorMin));
stateObject.setState(state);
} else {
logger.warn("Unhandled command type: {}", command.getClass().getName());
return;
}
break;
case CHANNEL_COLOR_TEMPERATURE_ABS:
if (command instanceof DecimalType) {
// Color temperature (absolute)
IntegerState state = new Ct();
state.setValue(((DecimalType) command).intValue());
stateObject.setState(state);
} else {
logger.warn("Unhandled command type: {}", command.getClass().getName());
return;
}
break;
case CHANNEL_PANEL_LAYOUT:
@Nullable
Layout layout = controllerInfo.getPanelLayout().getLayout();
String layoutView = (layout != null) ? layout.getLayoutView() : "";
logger.info("Panel layout and ids for controller {} \n{}", thing.getUID(), layoutView);
updateState(CHANNEL_PANEL_LAYOUT, OnOffType.OFF);
break;
default:
logger.warn("Unhandled command type: {}", command.getClass().getName());
return;
}
Request setNewStateRequest = OpenAPIUtils.requestBuilder(httpClient, getControllerConfig(), API_SET_VALUE,
HttpMethod.PUT);
setNewStateRequest.content(new StringContentProvider(gson.toJson(stateObject)), "application/json");
OpenAPIUtils.sendOpenAPIRequest(setNewStateRequest);
}
private void sendEffectCommand(Command command) throws NanoleafException {
Effects effects = new Effects();
if (command instanceof StringType) {
effects.setSelect(command.toString());
} else {
logger.warn("Unhandled command type: {}", command.getClass().getName());
return;
}
Request setNewEffectRequest = OpenAPIUtils.requestBuilder(httpClient, getControllerConfig(), API_EFFECT,
HttpMethod.PUT);
String content = gson.toJson(effects);
logger.debug("sending effect command from controller {}: {}", getThing().getUID(), content);
setNewEffectRequest.content(new StringContentProvider(content), "application/json");
OpenAPIUtils.sendOpenAPIRequest(setNewEffectRequest);
}
private void sendRhythmCommand(Command command) throws NanoleafException {
Rhythm rhythm = new Rhythm();
if (command instanceof DecimalType) {
rhythm.setRhythmMode(((DecimalType) command).intValue());
} else {
logger.warn("Unhandled command type: {}", command.getClass().getName());
return;
}
Request setNewRhythmRequest = OpenAPIUtils.requestBuilder(httpClient, getControllerConfig(), API_RHYTHM_MODE,
HttpMethod.PUT);
setNewRhythmRequest.content(new StringContentProvider(gson.toJson(rhythm)), "application/json");
OpenAPIUtils.sendOpenAPIRequest(setNewRhythmRequest);
}
private String getAddress() {
return StringUtils.defaultString(this.address);
}
private void setAddress(String address) {
this.address = address;
}
private int getPort() {
return port;
}
private void setPort(int port) {
this.port = port;
}
private int getRefreshIntervall() {
return refreshIntervall;
}
private void setRefreshIntervall(int refreshIntervall) {
this.refreshIntervall = refreshIntervall;
}
private String getAuthToken() {
return StringUtils.defaultString(authToken);
}
private void setAuthToken(@Nullable String authToken) {
this.authToken = authToken;
}
private String getDeviceType() {
return StringUtils.defaultString(deviceType);
}
private void setDeviceType(String deviceType) {
this.deviceType = deviceType;
}
private void stopAllJobs() {
stopPairingJob();
stopUpdateJob();
stopPanelDiscoveryJob();
stopTouchJob();
}
}

View File

@@ -0,0 +1,379 @@
/**
* Copyright (c) 2010-2020 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.handler;
import static org.openhab.binding.nanoleaf.internal.NanoleafBindingConstants.*;
import java.math.BigDecimal;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.client.util.StringContentProvider;
import org.eclipse.jetty.http.HttpMethod;
import org.openhab.binding.nanoleaf.internal.*;
import org.openhab.binding.nanoleaf.internal.config.NanoleafControllerConfig;
import org.openhab.binding.nanoleaf.internal.model.Effects;
import org.openhab.binding.nanoleaf.internal.model.Write;
import org.openhab.core.library.types.*;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.ThingStatusInfo;
import org.openhab.core.thing.binding.BaseThingHandler;
import org.openhab.core.thing.binding.BridgeHandler;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
/**
* The {@link NanoleafPanelHandler} is responsible for handling commands to the controller which
* affect an individual panels
*
* @author Martin Raepple - Initial contribution
* @author Stefan Höhn - Canvas Touch Support
*/
@NonNullByDefault
public class NanoleafPanelHandler extends BaseThingHandler {
private static final PercentType MIN_PANEL_BRIGHTNESS = PercentType.ZERO;
private static final PercentType MAX_PANEL_BRIGHTNESS = PercentType.HUNDRED;
private final Logger logger = LoggerFactory.getLogger(NanoleafPanelHandler.class);
private HttpClient httpClient;
// JSON parser for API responses
private final Gson gson = new Gson();
// holds current color data per panel
private Map<String, HSBType> panelInfo = new HashMap<>();
private @NonNullByDefault({}) ScheduledFuture<?> singleTapJob;
private @NonNullByDefault({}) ScheduledFuture<?> doubleTapJob;
public NanoleafPanelHandler(Thing thing, HttpClient httpClient) {
super(thing);
this.httpClient = httpClient;
}
@Override
public void initialize() {
logger.debug("Initializing handler for panel {}", getThing().getUID());
updateStatus(ThingStatus.OFFLINE);
Bridge controller = getBridge();
if (controller == null) {
initializePanel(new ThingStatusInfo(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED, ""));
} else if (ThingStatus.OFFLINE.equals(controller.getStatus())) {
initializePanel(new ThingStatusInfo(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE,
"@text/error.nanoleaf.panel.controllerOffline"));
} else {
initializePanel(controller.getStatusInfo());
}
}
@Override
public void bridgeStatusChanged(ThingStatusInfo controllerStatusInfo) {
logger.debug("Controller status changed to {} -- {}", controllerStatusInfo,
controllerStatusInfo.getDescription() + "/" + controllerStatusInfo.getStatus() + "/"
+ controllerStatusInfo.hashCode());
if (controllerStatusInfo.getStatus().equals(ThingStatus.OFFLINE)) {
initializePanel(new ThingStatusInfo(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE,
"@text/error.nanoleaf.panel.controllerOffline"));
} else {
initializePanel(controllerStatusInfo);
}
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
logger.debug("Received command {} for channel {}", command, channelUID);
try {
switch (channelUID.getId()) {
case CHANNEL_PANEL_COLOR:
sendRenderedEffectCommand(command);
break;
default:
logger.warn("Channel with id {} not handled", channelUID.getId());
break;
}
} catch (NanoleafUnauthorizedException nae) {
logger.warn("Authorization for command {} for channelUID {} failed: {}", command, channelUID,
nae.getMessage());
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"@text/error.nanoleaf.controller.invalidToken");
} catch (NanoleafException ne) {
logger.warn("Handling command {} for channelUID {} failed: {}", command, channelUID, ne.getMessage());
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"@text/error.nanoleaf.controller.communication");
}
}
@Override
public void handleRemoval() {
logger.debug("Nanoleaf panel {} removed", getThing().getUID());
super.handleRemoval();
}
@Override
public void dispose() {
logger.debug("Disposing handler for Nanoleaf panel {}", getThing().getUID());
stopAllJobs();
super.dispose();
}
private void stopAllJobs() {
if (singleTapJob != null && !singleTapJob.isCancelled()) {
logger.debug("Stop single touch job");
singleTapJob.cancel(true);
this.singleTapJob = null;
}
if (doubleTapJob != null && !doubleTapJob.isCancelled()) {
logger.debug("Stop double touch job");
doubleTapJob.cancel(true);
this.doubleTapJob = null;
}
}
private void initializePanel(ThingStatusInfo panelStatus) {
updateStatus(panelStatus.getStatus(), panelStatus.getStatusDetail());
logger.debug("Panel {} status changed to {}-{}", this.getThing().getUID(), panelStatus.getStatus(),
panelStatus.getStatusDetail());
}
private void sendRenderedEffectCommand(Command command) throws NanoleafException {
logger.debug("Command Type: {}", command.getClass());
HSBType currentPanelColor = getPanelColor();
if (currentPanelColor != null)
logger.debug("currentPanelColor: {}", currentPanelColor.toString());
HSBType newPanelColor = new HSBType();
if (command instanceof HSBType) {
newPanelColor = (HSBType) command;
} else if (command instanceof OnOffType && (currentPanelColor != null)) {
if (OnOffType.ON.equals(command)) {
newPanelColor = new HSBType(currentPanelColor.getHue(), currentPanelColor.getSaturation(),
MAX_PANEL_BRIGHTNESS);
} else {
newPanelColor = new HSBType(currentPanelColor.getHue(), currentPanelColor.getSaturation(),
MIN_PANEL_BRIGHTNESS);
}
} else if (command instanceof PercentType && (currentPanelColor != null)) {
PercentType brightness = new PercentType(
Math.max(MIN_PANEL_BRIGHTNESS.intValue(), ((PercentType) command).intValue()));
newPanelColor = new HSBType(currentPanelColor.getHue(), currentPanelColor.getSaturation(), brightness);
} else if (command instanceof IncreaseDecreaseType && (currentPanelColor != null)) {
int brightness = currentPanelColor.getBrightness().intValue();
if (command.equals(IncreaseDecreaseType.INCREASE)) {
brightness = Math.min(MAX_PANEL_BRIGHTNESS.intValue(), brightness + BRIGHTNESS_STEP_SIZE);
} else {
brightness = Math.max(MIN_PANEL_BRIGHTNESS.intValue(), brightness - BRIGHTNESS_STEP_SIZE);
}
newPanelColor = new HSBType(currentPanelColor.getHue(), currentPanelColor.getSaturation(),
new PercentType(brightness));
} else if (command instanceof RefreshType) {
logger.debug("Refresh command received");
return;
} else {
logger.warn("Unhandled command type: {}", command.getClass().getName());
return;
}
// store panel's new HSB value
logger.trace("Setting new color {}", newPanelColor);
panelInfo.put(getThing().getConfiguration().get(CONFIG_PANEL_ID).toString(), newPanelColor);
// transform to RGB
PercentType[] rgbPercent = newPanelColor.toRGB();
logger.trace("Setting new rgbpercent {} {} {}", rgbPercent[0], rgbPercent[1], rgbPercent[2]);
int red = rgbPercent[0].toBigDecimal().divide(BigDecimal.valueOf(100), 2, BigDecimal.ROUND_HALF_UP)
.multiply(new BigDecimal(255)).intValue();
int green = rgbPercent[1].toBigDecimal().divide(BigDecimal.valueOf(100), 2, BigDecimal.ROUND_HALF_UP)
.multiply(new BigDecimal(255)).intValue();
int blue = rgbPercent[2].toBigDecimal().divide(BigDecimal.valueOf(100), 2, BigDecimal.ROUND_HALF_UP)
.multiply(new BigDecimal(255)).intValue();
logger.trace("Setting new rgb {} {} {}", red, green, blue);
Bridge bridge = getBridge();
if (bridge != null) {
Effects effects = new Effects();
Write write = new Write();
write.setCommand("display");
write.setAnimType("static");
String panelID = this.thing.getConfiguration().get(CONFIG_PANEL_ID).toString();
@Nullable
BridgeHandler handler = bridge.getHandler();
if (handler != null) {
NanoleafControllerConfig config = ((NanoleafControllerHandler) handler).getControllerConfig();
// Light Panels and Canvas use different stream commands
if (config.deviceType.equals(CONFIG_DEVICE_TYPE_LIGHTPANELS)
|| config.deviceType.equals(CONFIG_DEVICE_TYPE_CANVAS)) {
logger.trace("Anim Data rgb {} {} {} {}", panelID, red, green, blue);
write.setAnimData(String.format("1 %s 1 %d %d %d 0 10", panelID, red, green, blue));
} else {
// this is only used in special streaming situations with canvas which is not yet supported
int quotient = Integer.divideUnsigned(Integer.valueOf(panelID), 256);
int remainder = Integer.remainderUnsigned(Integer.valueOf(panelID), 256);
write.setAnimData(
String.format("0 1 %d %d %d %d %d 0 0 10", quotient, remainder, red, green, blue));
}
write.setLoop(false);
effects.setWrite(write);
Request setNewRenderedEffectRequest = OpenAPIUtils.requestBuilder(httpClient, config, API_EFFECT,
HttpMethod.PUT);
String content = gson.toJson(effects);
logger.debug("sending effect command from panel {}: {}", getThing().getUID(), content);
setNewRenderedEffectRequest.content(new StringContentProvider(content), "application/json");
OpenAPIUtils.sendOpenAPIRequest(setNewRenderedEffectRequest);
} else {
logger.warn("Couldn't set rendering effect as Bridge-Handler {} is null", bridge.getUID());
}
}
}
public void updatePanelColorChannel() {
@Nullable
HSBType panelColor = getPanelColor();
logger.trace("updatePanelColorChannel: panelColor: {}", panelColor);
if (panelColor != null)
updateState(CHANNEL_PANEL_COLOR, panelColor);
}
/**
* Apply the gesture to the panel
*
* @param gesture Only 0=single tap and 1=double tap are supported
*/
public void updatePanelGesture(int gesture) {
switch (gesture) {
case 0:
updateState(CHANNEL_PANEL_SINGLE_TAP, OnOffType.ON);
singleTapJob = scheduler.schedule(this::resetSingleTap, 1, TimeUnit.SECONDS);
logger.debug("Asserting single tap of panel {} to ON", getPanelID());
break;
case 1:
updateState(CHANNEL_PANEL_DOUBLE_TAP, OnOffType.ON);
doubleTapJob = scheduler.schedule(this::resetDoubleTap, 1, TimeUnit.SECONDS);
logger.debug("Asserting double tap of panel {} to ON", getPanelID());
break;
}
}
private void resetSingleTap() {
updateState(CHANNEL_PANEL_SINGLE_TAP, OnOffType.OFF);
logger.debug("Resetting single tap of panel {} to OFF", getPanelID());
}
private void resetDoubleTap() {
updateState(CHANNEL_PANEL_DOUBLE_TAP, OnOffType.OFF);
logger.debug("Resetting double tap of panel {} to OFF", getPanelID());
}
public String getPanelID() {
String panelID = getThing().getConfiguration().get(CONFIG_PANEL_ID).toString();
return panelID;
}
private @Nullable HSBType getPanelColor() {
String panelID = getPanelID();
// get panel color data from controller
try {
Effects effects = new Effects();
Write write = new Write();
write.setCommand("request");
write.setAnimName("*Static*");
effects.setWrite(write);
Bridge bridge = getBridge();
if (bridge != null) {
NanoleafControllerHandler handler = (NanoleafControllerHandler) bridge.getHandler();
if (handler != null) {
NanoleafControllerConfig config = handler.getControllerConfig();
logger.debug("Sending Request from Panel for getColor()");
Request setPanelUpdateRequest = OpenAPIUtils.requestBuilder(httpClient, config, API_EFFECT,
HttpMethod.PUT);
setPanelUpdateRequest.content(new StringContentProvider(gson.toJson(effects)), "application/json");
ContentResponse panelData = OpenAPIUtils.sendOpenAPIRequest(setPanelUpdateRequest);
// parse panel data
parsePanelData(panelID, config, panelData);
}
}
} catch (NanoleafNotFoundException nfe) {
logger.warn("Panel data could not be retrieved as no data was returned (static type missing?) : {}",
nfe.getMessage());
} catch (NanoleafBadRequestException nfe) {
logger.debug(
"Panel data could not be retrieved as request not expected(static type missing / dynamic type on) : {}",
nfe.getMessage());
} catch (NanoleafException nue) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"@text/error.nanoleaf.panel.communication");
logger.warn("Panel data could not be retrieved: {}", nue.getMessage());
}
return panelInfo.get(panelID);
}
void parsePanelData(String panelID, NanoleafControllerConfig config, ContentResponse panelData) {
// panelData is in format (numPanels, (PanelId, 1, R, G, B, W, TransitionTime) * numPanel)
@Nullable
Write response = gson.fromJson(panelData.getContentAsString(), Write.class);
if (response != null) {
String[] tokenizedData = response.getAnimData().split(" ");
if (config.deviceType.equals(CONFIG_DEVICE_TYPE_LIGHTPANELS)
|| config.deviceType.equals(CONFIG_DEVICE_TYPE_CANVAS)) {
// panelData is in format (numPanels (PanelId 1 R G B W TransitionTime) * numPanel)
String[] panelDataPoints = Arrays.copyOfRange(tokenizedData, 1, tokenizedData.length);
for (int i = 0; i < panelDataPoints.length; i++) {
if (i % 7 == 0) {
String id = panelDataPoints[i];
if (id.equals(panelID)) {
// found panel data - store it
panelInfo.put(panelID,
HSBType.fromRGB(Integer.parseInt(panelDataPoints[i + 2]),
Integer.parseInt(panelDataPoints[i + 3]),
Integer.parseInt(panelDataPoints[i + 4])));
}
}
}
} else {
// panelData is in format (0 numPanels (quotient(panelID) remainder(panelID) R G B W 0
// quotient(TransitionTime) remainder(TransitionTime)) * numPanel)
String[] panelDataPoints = Arrays.copyOfRange(tokenizedData, 2, tokenizedData.length);
for (int i = 0; i < panelDataPoints.length; i++) {
if (i % 8 == 0) {
String idQuotient = panelDataPoints[i];
String idRemainder = panelDataPoints[i + 1];
Integer idNum = Integer.valueOf(idQuotient) * 256 + Integer.valueOf(idRemainder);
if (String.valueOf(idNum).equals(panelID)) {
// found panel data - store it
panelInfo.put(panelID,
HSBType.fromRGB(Integer.parseInt(panelDataPoints[i + 3]),
Integer.parseInt(panelDataPoints[i + 4]),
Integer.parseInt(panelDataPoints[i + 5])));
}
}
}
}
}
}
}

View File

@@ -0,0 +1,38 @@
/**
* Copyright (c) 2010-2020 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.model;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import com.google.gson.annotations.SerializedName;
/**
* Represents an Authorization Token
*
* @author Martin Raepple - Initial contribution
*/
@NonNullByDefault
public class AuthToken {
@SerializedName("auth_token")
private @Nullable String authToken;
public @Nullable String getAuthToken() {
return authToken;
}
public void setAuthToken(String authToken) {
this.authToken = authToken;
}
}

View File

@@ -0,0 +1,28 @@
/**
* Copyright (c) 2010-2020 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.model;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Interface for boolean value states
*
* @author Martin Raepple - Initial contribution
*/
@NonNullByDefault
public interface BooleanState {
boolean getValue();
void setValue(boolean value);
}

View File

@@ -0,0 +1,55 @@
/**
* Copyright (c) 2010-2020 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.model;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* Represents brightness setting of the light panels
*
* @author Martin Raepple - Initial contribution
*/
@NonNullByDefault
public class Brightness implements IntegerState {
private int value;
private @Nullable Integer max;
private @Nullable Integer min;
@Override
public int getValue() {
return value;
}
@Override
public void setValue(int value) {
this.value = value;
}
public @Nullable Integer getMax() {
return max;
}
public void setMax(Integer max) {
this.max = max;
}
public @Nullable Integer getMin() {
return min;
}
public void setMin(Integer min) {
this.min = min;
}
}

View File

@@ -0,0 +1,38 @@
/**
* Copyright (c) 2010-2020 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.model;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import com.google.gson.annotations.SerializedName;
/**
* Represents color temperature of the light panels
*
* @author Martin Raepple - Initial contribution
*/
@NonNullByDefault
public class Command {
@SerializedName("write")
private @Nullable Write write;
public @Nullable Write getWrite() {
return write;
}
public void setWrite(Write write) {
this.write = write;
}
}

View File

@@ -0,0 +1,106 @@
/**
* Copyright (c) 2010-2020 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.model;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Represents the light panels controller information
*
* @author Martin Raepple - Initial contribution
*/
@NonNullByDefault
public class ControllerInfo {
private String name = "";
private String serialNo = "";
private String manufacturer = "";
private String firmwareVersion = "";
private String model = "";
private State state = new State();
private Effects effects = new Effects();
private PanelLayout panelLayout = new PanelLayout();
private Rhythm rhythm = new Rhythm();
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getSerialNo() {
return serialNo;
}
public void setSerialNo(String serialNo) {
this.serialNo = serialNo;
}
public String getManufacturer() {
return manufacturer;
}
public void setManufacturer(String manufacturer) {
this.manufacturer = manufacturer;
}
public String getFirmwareVersion() {
return firmwareVersion;
}
public void setFirmwareVersion(String firmwareVersion) {
this.firmwareVersion = firmwareVersion;
}
public String getModel() {
return model;
}
public void setModel(String model) {
this.model = model;
}
public State getState() {
return state;
}
public void setState(State state) {
this.state = state;
}
public Effects getEffects() {
return effects;
}
public void setEffects(Effects effects) {
this.effects = effects;
}
public PanelLayout getPanelLayout() {
return panelLayout;
}
public void setPanelLayout(PanelLayout panelLayout) {
this.panelLayout = panelLayout;
}
public Rhythm getRhythm() {
return rhythm;
}
public void setRhythm(Rhythm rhythm) {
this.rhythm = rhythm;
}
}

View File

@@ -0,0 +1,55 @@
/**
* Copyright (c) 2010-2020 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.model;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* Represents color temperature of the light panels
*
* @author Martin Raepple - Initial contribution
*/
@NonNullByDefault
public class Ct implements IntegerState {
private int value;
private @Nullable Integer max;
private @Nullable Integer min;
public @Nullable Integer getMax() {
return max;
}
public void setMax(@Nullable Integer max) {
this.max = max;
}
public @Nullable Integer getMin() {
return min;
}
public void setMin(@Nullable Integer min) {
this.min = min;
}
@Override
public int getValue() {
return value;
}
@Override
public void setValue(int value) {
this.value = value;
}
}

View File

@@ -0,0 +1,55 @@
/**
* Copyright (c) 2010-2020 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.model;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* Represents effect commands for select and write
*
* @author Martin Raepple - Initial contribution
*/
@NonNullByDefault
public class Effects {
private @Nullable String select;
private @Nullable List<String> effectsList = null;
private @Nullable Write write;
public @Nullable String getSelect() {
return select;
}
public void setSelect(@Nullable String select) {
this.select = select;
}
public @Nullable List<String> getEffectsList() {
return effectsList;
}
public void setEffectsList(@Nullable List<String> effectsList) {
this.effectsList = effectsList;
}
public @Nullable Write getWrite() {
return write;
}
public void setWrite(@Nullable Write write) {
this.write = write;
}
}

View File

@@ -0,0 +1,53 @@
/**
* Copyright (c) 2010-2020 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.model;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* Represents global orientation settings of the light panels
*
* @author Martin Raepple - Initial contribution
*/
@NonNullByDefault
public class GlobalOrientation {
private int value;
private @Nullable Integer max;
private @Nullable Integer min;
public int getValue() {
return value;
}
public void setValue(int value) {
this.value = value;
}
public @Nullable Integer getMax() {
return max;
}
public void setMax(Integer max) {
this.max = max;
}
public @Nullable Integer getMin() {
return min;
}
public void setMin(Integer min) {
this.min = min;
}
}

View File

@@ -0,0 +1,55 @@
/**
* Copyright (c) 2010-2020 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.model;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* Represents hue setting of the light panels
*
* @author Martin Raepple - Initial contribution
*/
@NonNullByDefault
public class Hue implements IntegerState {
private int value;
private @Nullable Integer max;
private @Nullable Integer min;
@Override
public int getValue() {
return value;
}
@Override
public void setValue(int value) {
this.value = value;
}
public @Nullable Integer getMax() {
return max;
}
public void setMax(Integer max) {
this.max = max;
}
public @Nullable Integer getMin() {
return min;
}
public void setMin(Integer min) {
this.min = min;
}
}

View File

@@ -0,0 +1,28 @@
/**
* Copyright (c) 2010-2020 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.model;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Interface for settings with integer value
*
* @author Martin Raepple - Initial contribution
*/
@NonNullByDefault
public interface IntegerState {
void setValue(int value);
int getValue();
}

View File

@@ -0,0 +1,131 @@
/**
* Copyright (c) 2010-2020 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.model;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* Represents layout of the light panels
*
* @author Martin Raepple - Initial contribution
*/
@NonNullByDefault
public class Layout {
private int numPanels;
private int sideLength;
private @Nullable List<PositionDatum> positionData = null;
public int getNumPanels() {
return numPanels;
}
public void setNumPanels(int numPanels) {
this.numPanels = numPanels;
}
public int getSideLength() {
return sideLength;
}
public void setSideLength(int sideLength) {
this.sideLength = sideLength;
}
public @Nullable List<PositionDatum> getPositionData() {
return positionData;
}
public void setPositionData(List<PositionDatum> positionData) {
this.positionData = positionData;
}
/**
* Returns an text representation for a canvas layout.
*
* Note only canvas supported currently due to its easy geometry
*
* @return a String containing the layout
*/
public String getLayoutView() {
if (positionData != null) {
String view = "";
int minx = Integer.MAX_VALUE;
int maxx = Integer.MIN_VALUE;
int miny = Integer.MAX_VALUE;
int maxy = Integer.MIN_VALUE;
final int noofDefinedPanels = positionData.size();
for (int index = 0; index < noofDefinedPanels; index++) {
if (positionData != null) {
@Nullable
PositionDatum panel = positionData.get(index);
if (panel != null) {
if (panel.getPosX() < minx) {
minx = panel.getPosX();
}
if (panel.getPosX() > maxx) {
maxx = panel.getPosX();
}
if (panel.getPosY() < miny) {
miny = panel.getPosY();
}
if (panel.getPosY() > maxy) {
maxy = panel.getPosY();
}
}
}
}
int shiftWidth = getSideLength() / 2;
int lineY = maxy;
Map<Integer, PositionDatum> map;
while (lineY >= miny) {
map = new TreeMap<>();
for (int index = 0; index < noofDefinedPanels; index++) {
if (positionData != null) {
@Nullable
PositionDatum panel = positionData.get(index);
if (panel != null && panel.getPosY() == lineY)
map.put(panel.getPosX(), panel);
}
}
lineY -= shiftWidth;
for (int x = minx; x <= maxx; x += shiftWidth) {
if (map.containsKey(x)) {
@Nullable
PositionDatum panel = map.get(x);
view += String.format("%5s ", panel.getPanelId());
} else
view += " ";
}
view += "\n";
}
return view;
} else
return "";
}
}

View File

@@ -0,0 +1,36 @@
/**
* Copyright (c) 2010-2020 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.model;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Represents power state of the light panels
*
* @author Martin Raepple - Initial contribution
*/
@NonNullByDefault
public class On implements BooleanState {
private boolean value;
@Override
public boolean getValue() {
return value;
}
@Override
public void setValue(boolean value) {
this.value = value;
}
}

View File

@@ -0,0 +1,52 @@
/**
* Copyright (c) 2010-2020 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.model;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Represents color palette in the write command
*
* @author Martin Raepple - Initial contribution
*/
@NonNullByDefault
public class Palette {
private int hue;
private int saturation;
private int brightness;
public int getHue() {
return hue;
}
public void setHue(int hue) {
this.hue = hue;
}
public int getSaturation() {
return saturation;
}
public void setSaturation(int saturation) {
this.saturation = saturation;
}
public int getBrightness() {
return brightness;
}
public void setBrightness(int brightness) {
this.brightness = brightness;
}
}

View File

@@ -0,0 +1,44 @@
/**
* Copyright (c) 2010-2020 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.model;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* Represents panel layout of the light panels
*
* @author Martin Raepple - Initial contribution
*/
@NonNullByDefault
public class PanelLayout {
private @Nullable Layout layout;
private @Nullable GlobalOrientation globalOrientation;
public @Nullable Layout getLayout() {
return layout;
}
public void setLayout(Layout layout) {
this.layout = layout;
}
public @Nullable GlobalOrientation getGlobalOrientation() {
return globalOrientation;
}
public void setGlobalOrientation(GlobalOrientation globalOrientation) {
this.globalOrientation = globalOrientation;
}
}

View File

@@ -0,0 +1,66 @@
/**
* Copyright (c) 2010-2020 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.model;
import org.eclipse.jdt.annotation.NonNullByDefault;
import com.google.gson.annotations.SerializedName;
/**
* Represents panel position
*
* @author Martin Raepple - Initial contribution
*/
@NonNullByDefault
public class PositionDatum {
private int panelId;
@SerializedName("x")
private int posX;
@SerializedName("y")
private int posY;
@SerializedName("o")
private int orientation;
public int getPanelId() {
return panelId;
}
public void setPanelId(int panelId) {
this.panelId = panelId;
}
public int getPosX() {
return posX;
}
public void setPosX(int x) {
this.posX = x;
}
public int getPosY() {
return posY;
}
public void setPosY(int y) {
this.posY = y;
}
public int getOrientation() {
return orientation;
}
public void setOrientation(int o) {
this.orientation = o;
}
}

View File

@@ -0,0 +1,98 @@
/**
* Copyright (c) 2010-2020 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.model;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* Represents rhythm module settings
*
* @author Martin Raepple - Initial contribution
*/
@NonNullByDefault
public class Rhythm {
private boolean rhythmConnected;
private boolean rhythmActive;
private int rhythmId;
private String hardwareVersion = "";
private String firmwareVersion = "";
private boolean auxAvailable;
private int rhythmMode;
private @Nullable RhythmPos rhythmPos;
public boolean getRhythmConnected() {
return rhythmConnected;
}
public void setRhythmConnected(boolean rhythmConnected) {
this.rhythmConnected = rhythmConnected;
}
public boolean getRhythmActive() {
return rhythmActive;
}
public void setRhythmActive(boolean rhythmActive) {
this.rhythmActive = rhythmActive;
}
public int getRhythmId() {
return rhythmId;
}
public void setRhythmId(int rhythmId) {
this.rhythmId = rhythmId;
}
public String getHardwareVersion() {
return this.hardwareVersion;
}
public void setHardwareVersion(String hardwareVersion) {
this.hardwareVersion = hardwareVersion;
}
public String getFirmwareVersion() {
return this.firmwareVersion;
}
public void setFirmwareVersion(String firmwareVersion) {
this.firmwareVersion = firmwareVersion;
}
public boolean getAuxAvailable() {
return auxAvailable;
}
public void setAuxAvailable(boolean auxAvailable) {
this.auxAvailable = auxAvailable;
}
public int getRhythmMode() {
return rhythmMode;
}
public void setRhythmMode(int rhythmMode) {
this.rhythmMode = rhythmMode;
}
public @Nullable RhythmPos getRhythmPos() {
return rhythmPos;
}
public void setRhythmPos(RhythmPos rhythmPos) {
this.rhythmPos = rhythmPos;
}
}

View File

@@ -0,0 +1,57 @@
/**
* Copyright (c) 2010-2020 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.model;
import org.eclipse.jdt.annotation.NonNullByDefault;
import com.google.gson.annotations.SerializedName;
/**
* Represents rhythm module position
*
* @author Martin Raepple - Initial contribution
*/
@NonNullByDefault
public class RhythmPos {
@SerializedName("x")
private float posX;
@SerializedName("y")
private float posY;
@SerializedName("o")
private float orientation;
public float getPosX() {
return posX;
}
public void setPosX(float x) {
this.posX = x;
}
public float getPosY() {
return posY;
}
public void setPosY(float y) {
this.posY = y;
}
public float getOrientation() {
return orientation;
}
public void setOrientation(float o) {
this.orientation = o;
}
}

View File

@@ -0,0 +1,55 @@
/**
* Copyright (c) 2010-2020 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.model;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* Represents saturation setting of the light panels
*
* @author Martin Raepple - Initial contribution
*/
@NonNullByDefault
public class Sat implements IntegerState {
private int value;
private @Nullable Integer max;
private @Nullable Integer min;
@Override
public int getValue() {
return value;
}
@Override
public void setValue(int value) {
this.value = value;
}
public @Nullable Integer getMax() {
return max;
}
public void setMax(Integer max) {
this.max = max;
}
public @Nullable Integer getMin() {
return min;
}
public void setMin(Integer min) {
this.min = min;
}
}

View File

@@ -0,0 +1,108 @@
/**
* Copyright (c) 2010-2020 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.model;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.library.types.OnOffType;
import com.google.gson.annotations.SerializedName;
/**
* Represents overall state settings of the light panels
*
* @author Martin Raepple - Initial contribution
*/
@NonNullByDefault
public class State {
private @Nullable On on;
private @Nullable Brightness brightness;
private @Nullable Hue hue;
@SerializedName("sat")
private @Nullable Sat saturation;
@SerializedName("ct")
private @Nullable Ct colorTemperature;
private @Nullable String colorMode;
public @Nullable On getOn() {
return on;
}
public OnOffType getOnOff() {
return (on != null && on.getValue()) ? OnOffType.ON : OnOffType.OFF;
}
public void setOn(On on) {
this.on = on;
}
public @Nullable Brightness getBrightness() {
return brightness;
}
public void setBrightness(Brightness brightness) {
this.brightness = brightness;
}
public @Nullable Hue getHue() {
return hue;
}
public void setHue(Hue hue) {
this.hue = hue;
}
public @Nullable Sat getSaturation() {
return saturation;
}
public void setSaturation(Sat sat) {
this.saturation = sat;
}
public @Nullable Ct getColorTemperature() {
return colorTemperature;
}
public void setColorTemperature(Ct ct) {
this.colorTemperature = ct;
}
public @Nullable String getColorMode() {
return colorMode;
}
public void setColorMode(String colorMode) {
this.colorMode = colorMode;
}
public void setState(IntegerState value) {
if (value instanceof Brightness) {
this.setBrightness((Brightness) value);
} else if (value instanceof Hue) {
this.setHue((Hue) value);
} else if (value instanceof Sat) {
this.setSaturation((Sat) value);
} else if (value instanceof Ct) {
this.setColorTemperature((Ct) value);
}
}
public void setState(BooleanState value) {
if (value instanceof On) {
this.setOn((On) value);
}
}
}

View File

@@ -0,0 +1,55 @@
/**
* Copyright (c) 2010-2020 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.model;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Nanoleaf Panel TouchEvent provided by controller
*
*
* JSON
* {"events":
* [
* { "panelId":48111,
* "gesture":0},
* { "panelId":48112,
* * "gesture":1}
* ]
* }
*
*
* @author Stefan Höhn - Initial contribution
*/
@NonNullByDefault
public class TouchEvent {
private String panelId = "";
private int gesture = -1;
public String getPanelId() {
return panelId;
}
public void setPanelId(String panelId) {
this.panelId = panelId;
}
public int getGesture() {
return gesture;
}
public void setGesture(int gesture) {
this.gesture = gesture;
}
}

View File

@@ -0,0 +1,38 @@
/**
* Copyright (c) 2010-2020 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.model;
import java.util.ArrayList;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Represents effect commands for select and write
*
* @author Stefan Höhn - Initial contribution
*/
@NonNullByDefault
public class TouchEvents {
private List<TouchEvent> events = new ArrayList<>();
public List<TouchEvent> getEvents() {
return events;
}
public void setEvents(List<TouchEvent> events) {
this.events = events;
}
}

View File

@@ -0,0 +1,91 @@
/**
* Copyright (c) 2010-2020 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.model;
import java.util.ArrayList;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Represents write command to set solid color effect
*
* @author Martin Raepple - Initial contribution
*/
@NonNullByDefault
public class Write {
private String command = "";
private String animType = "";
private String animName = "";
private List<Palette> palette = new ArrayList<>();
private String colorType = "";
private String animData = "";
private boolean loop = false;
public String getCommand() {
return command;
}
public void setCommand(String command) {
this.command = command;
}
public String getAnimType() {
return animType;
}
public void setAnimType(String animType) {
this.animType = animType;
}
public List<Palette> getPalette() {
return palette;
}
public void setPalette(List<Palette> palette) {
this.palette = palette;
}
public String getColorType() {
return colorType;
}
public void setColorType(String colorType) {
this.colorType = colorType;
}
public String getAnimData() {
return animData;
}
public void setAnimData(String animData) {
this.animData = animData;
}
public boolean getLoop() {
return loop;
}
public void setLoop(boolean loop) {
this.loop = loop;
}
public String getAnimName() {
return animName;
}
public void setAnimName(String animName) {
this.animName = animName;
}
}

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<binding:binding id="nanoleaf" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:binding="https://openhab.org/schemas/binding/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/binding/v1.0.0 https://openhab.org/schemas/binding-1.0.0.xsd">
<name>@text/binding.nanoleaf.name</name>
<description>@text/binding.nanoleaf.description</description>
<author>Martin Raepple, Stefan Höhn</author>
</binding:binding>

View File

@@ -0,0 +1,46 @@
<?xml version="1.0" encoding="UTF-8"?>
<config-description:config-descriptions
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0 https://openhab.org/schemas/config-description-1.0.0.xsd">
<config-description uri="thing-type:nanoleaf:controller">
<parameter name="address" type="text" required="true">
<context>network-address</context>
<label>@text/thing-type.config.nanoleaf.controller.address.label</label>
<description>@text/thing-type.config.nanoleaf.controller.address.description</description>
</parameter>
<parameter name="port" type="integer" required="true" min="1" max="65535">
<label>@text/thing-type.config.nanoleaf.controller.port.label</label>
<description>@text/thing-type.config.nanoleaf.controller.port.description</description>
<default>16021</default>
</parameter>
<parameter name="authToken" type="text">
<context>password</context>
<label>@text/thing-type.config.nanoleaf.controller.authToken.label</label>
<description>@text/thing-type.config.nanoleaf.controller.authToken.description</description>
</parameter>
<parameter name="refreshInterval" type="integer" unit="s">
<label>@text/thing-type.config.nanoleaf.controller.refreshInterval.label</label>
<description>@text/thing-type.config.nanoleaf.controller.refreshInterval.description</description>
<default>60</default>
</parameter>
<parameter name="deviceType" type="text" readOnly="true">
<label>@text/thing-type.config.nanoleaf.controller.deviceType.label</label>
<description>@text/thing-type.config.nanoleaf.controller.deviceType.description</description>
<default>lightPanels</default>
<options>
<option value="lightPanels">Light Panels</option>
<option value="canvas">Canvas</option>
</options>
</parameter>
</config-description>
<config-description uri="thing-type:nanoleaf:lightpanel">
<parameter name="id" type="integer" required="true">
<label>@text/thing-type.config.nanoleaf.lightpanel.id.label</label>
<description>@text/thing-type.config.nanoleaf.lightpanel.id.description</description>
</parameter>
</config-description>
</config-description:config-descriptions>

View File

@@ -0,0 +1,65 @@
binding.nanoleaf.name = Nanoleaf Binding
binding.nanoleaf.description = Integrates the Nanoleaf Light Panels (v150320)
# thing types
thing-type.nanoleaf.controller.name = Nanoleaf Controller
thing-type.nanoleaf.controller.description = The Nanoleaf controller (bridge) device
thing-type.nanoleaf.lightpanel.name = Nanoleaf Panel
thing-type.nanoleaf.lightpanel.description = A panel connected to the Nanoleaf controller
# config
thing-type.config.nanoleaf.controller.address.label = IP Address Or Hostname
thing-type.config.nanoleaf.controller.address.description = IP address or hostname of the Nanoleaf controller, for example 192.168.0.10
thing-type.config.nanoleaf.controller.port.label = Port
thing-type.config.nanoleaf.controller.port.description = Port number of the Nanoleaf API, for example 16021
thing-type.config.nanoleaf.controller.authToken.label = Authorization Token
thing-type.config.nanoleaf.controller.authToken.description = Authorization token, required by openHAB to call the controller API. For pairing, press the on-off button of the controller for 5-7 seconds until the LED starts flashing in a pattern.
thing-type.config.nanoleaf.controller.refreshInterval.label = Refresh Interval
thing-type.config.nanoleaf.controller.refreshInterval.description = Interval (in seconds) to refresh the controller channels status
thing-type.config.nanoleaf.controller.deviceType.label = Nanoleaf Device Type
thing-type.config.nanoleaf.controller.deviceType.description = Light Panels (triangles) or Canvas (squares)
thing-type.config.nanoleaf.lightpanel.id.label = ID Of The Panel
thing-type.config.nanoleaf.lightpanel.id.description = The ID of the panel assigned by the controller
# channel types
channel-type.nanoleaf.power.label = Power
channel-type.nanoleaf.power.description = Power state of the controller
channel-type.nanoleaf.color.label = Color
channel-type.nanoleaf.color.description = Color setting for an individual or all panels
channel-type.nanoleaf.colorTemperature.label = Color Temperature
channel-type.nanoleaf.colorTemperature.description = Color temperature in percent
channel-type.nanoleaf.colorTemperatureAbs.label = Color Temperature (K)
channel-type.nanoleaf.colorTemperatureAbs.description = Color temperature in Kelvin (K)
channel-type.nanoleaf.colorMode.label = Color Mode
channel-type.nanoleaf.colorMode.description = Current color mode: Effect, hue/saturation or color temperature
channel-type.nanoleaf.effect.label = Effect
channel-type.nanoleaf.effect.description = Effect or scene currently playing
channel-type.nanoleaf.rhythmState.label = Rhythm State
channel-type.nanoleaf.rhythmState.description = Connection state of the rhythm module
channel-type.nanoleaf.rhythmActive.label = Rhythm Active
channel-type.nanoleaf.rhythmActive.description = Activity state of the rhythm module
channel-type.nanoleaf.rhythmMode.label = Rhythm Mode
channel-type.nanoleaf.rhythmMode.description = Sound source for the rhythm module (microphone or aux cable)
channel-type.nanoleaf.panelLayout.label = Panel Layout
channel-type.nanoleaf.panelLayout.description = Creates a panel layout upon request
channel-type.nanoleaf.panelColor.label = Panel Color
channel-type.nanoleaf.panelColor.description = Color of the individual panel
channel-type.nanoleaf.singleTap.label = SingleTap
channel-type.nanoleaf.singleTap.description = Single tap on the panel
channel-type.nanoleaf.doubleTap.label = DoubleTap
channel-type.nanoleaf.doubleTap.description = Double tap on the panel
# error messages
error.nanoleaf.controller.noIp = IP/host address and/or port are not configured for the controller.
error.nanoleaf.controller.incompatibleFirmware = Incompatible controller firmware. Remove the device, update the firmware, and add it again.
error.nanoleaf.controller.noToken = No authorization token found. To start pairing, press the on-off button of the controller for 5-7 seconds until the LED starts flashing in a pattern.
error.nanoleaf.controller.invalidToken = Invalid token. Replace with valid token or start pairing again by removing the invalid token from the configuration.
error.nanoleaf.controller.communication = Communication failed. Please check configuration.
error.nanoleaf.controller.pairingFailed = Pairing failed. Press the on-off button for 5-7 seconds until the LED starts flashing in a pattern.
error.nanoleaf.controller.invalidData = Pairing failed. Received invalid data
error.nanoleaf.controller.noTokenReceived = Pairing failed. No authorization token received from controller.
error.nanoleaf.controller.authRequest = Pairing failed. Cannot send authorization request.
error.nanoleaf.controller.noClient = Pairing failed. Cannot start HTTP client.
error.nanoleaf.controller.runtime = Runtime error. See openHAB log for more details.
error.nanoleaf.panel.communication = Panel data could not be retrieved. Please check controller configuration.
error.nanoleaf.panel.controllerOffline = Controller is offline. Check configuration.

View File

@@ -0,0 +1,65 @@
binding.nanoleaf.name = Nanoleaf Binding
binding.nanoleaf.description = Binding für die Integration des Nanoleaf Light Panels (v150320)
# thing types
thing-type.nanoleaf.controller.name = Nanoleaf Controller
thing-type.nanoleaf.controller.description = Nanoleaf Controller (Brücke)
thing-type.nanoleaf.lightpanel.name = Nanoleaf Paneel
thing-type.nanoleaf.lightpanel.description = Ein mit dem Controller verbundenes Paneel
# config
thing-type.config.nanoleaf.controller.address.label = IP Adresse oder Hostname
thing-type.config.nanoleaf.controller.address.description = IP Adresse oder Hostname des Nanoleaf Controllers, z. B. 192.168.0.10
thing-type.config.nanoleaf.controller.port.label = Port
thing-type.config.nanoleaf.controller.port.description = Portnummer des Controllers, z. B. 16021
thing-type.config.nanoleaf.controller.authToken.label = Authentifizierungstoken
thing-type.config.nanoleaf.controller.authToken.description = Token zur Authentifizierung. Um openHAB mit dem Nanoleaf Light Panels zu verbinden (Pairing) den An/Aus-Schalter am Controller für 5-7 Sekunden gedrückthalten, bis die LED zu blinken beginnt.
thing-type.config.nanoleaf.controller.refreshInterval.label = Aktualisierungsintervall
thing-type.config.nanoleaf.controller.refreshInterval.description = Intervall (in Sekunden) in dem die Kanäle aktualisiert werden
thing-type.config.nanoleaf.controller.deviceType.label = Nanoleaf Gerätetyp
thing-type.config.nanoleaf.controller.deviceType.description = Light Panels (Dreiecke) oder Canvas (Quadrate)
thing-type.config.nanoleaf.lightpanel.id.label = Paneel ID
thing-type.config.nanoleaf.lightpanel.id.description = Vom Controller vergebene ID eines Paneels
# channel types
channel-type.nanoleaf.power.label = Power
channel-type.nanoleaf.power.description = Ermöglicht das An- und Ausschalten des Nanoleaf Light Panels
channel-type.nanoleaf.color.label = Farbe
channel-type.nanoleaf.color.description = Farbe aller oder eines einzelnen Paneels
channel-type.nanoleaf.colorTemperature.label = Farbtemperatur
channel-type.nanoleaf.colorTemperature.description = Farbtemperatur in Prozent
channel-type.nanoleaf.colorTemperatureAbs.label = Farbtemperatur (K)
channel-type.nanoleaf.colorTemperatureAbs.description = Farbtemperatur in Kelvin (K)
channel-type.nanoleaf.colorMode.label = Farbmodus
channel-type.nanoleaf.colorMode.description = Effekt, Hue/Sat oder Farbtemperatur für alle Paneele.
channel-type.nanoleaf.effect.label = Effekt
channel-type.nanoleaf.effect.description = Einstellung des Effektes
channel-type.nanoleaf.rhythmState.label = Rhythm Status
channel-type.nanoleaf.rhythmState.description = Anschlusszustand des Rhythm Moduls
channel-type.nanoleaf.rhythmActive.label = Rhythm Aktiv
channel-type.nanoleaf.rhythmActive.description = Zeigt an ob das Mikrofon des Rhythm Modules ativ ist.
channel-type.nanoleaf.rhythmMode.label = Rhythm Modus
channel-type.nanoleaf.rhythmMode.description = Erlaubt den Wechsel zwischen eingebautem Mikrofon und AUX-Kabel.
channel-type.nanoleaf.panelLayout.label = PanelLayout
channel-type.nanoleaf.panelLayout.description = Erzeugt auf Anfrage ein Panel-Layout
channel-type.nanoleaf.panelColor.label = Paneelfarbe
channel-type.nanoleaf.panelColor.description = Farbe des einzelnen Paneels
channel-type.nanoleaf.singleTap.label = Einzel-Tap
channel-type.nanoleaf.singleTap.description = Panel wurde einmal angetippt
channel-type.nanoleaf.doubleTap.label = Doppel-Tap
channel-type.nanoleaf.doubleTap.description = Panel wurde zweimal hintereinander angetippt
# error messages
error.nanoleaf.controller.noIp = IP/Host-Adresse und/oder Port sind für den Controller nicht konfiguriert.
error.nanoleaf.controller.incompatibleFirmware = Inkompatible Controller-Firmware. Firmware aktualisieren und das Gerät neu hinzufügen.
error.nanoleaf.controller.noToken = Kein Authentifizierungstoken gefunden. Um das Pairing zu starten, den An/Aus-Knopf am Controller für 5-7 Sekunden lang gedrückt halten, bis die LED anfängt zu blinken.
error.nanoleaf.controller.invalidToken = Ungültiges Authentifizierungstoken. Token ändern oder das Pairing neu starten durch Löschen des ungültigen Tokens.
error.nanoleaf.controller.communication = Kommunikationsfehler. Konfiguration des Controllers prüfen.
error.nanoleaf.controller.pairingFailed = Pairing fehlgeschlagen. Um das Pairing zu starten, den An/Aus-Knopf am Controller für 5-7 Sekunden lang gedrückt halten, bis die LED anfängt zu blinken.
error.nanoleaf.controller.invalidData = Pairing fehlgeschlagen. Ungültige Daten vom Controller empfangen.
error.nanoleaf.controller.noTokenReceived = Pairing fehlgeschlagen. Kein Authentifizierungstoken empfangen.
error.nanoleaf.controller.authRequest = Pairing fehlgeschlagen. Berechtigungsanfrage konnte nicht an den Controller gesendet werden.
error.nanoleaf.controller.noClient = Pairing fehlgeschlagen. HTTP Client nicht verfügbar.
error.nanoleaf.controller.runtime = Laufzeitfehler. Siehe openHAB Logdatei für mehr Details.
error.nanoleaf.panel.communication = Paneeldaten konnten nicht empfangen werden. Konfiguration des Controllers prüfen.
error.nanoleaf.panel.controllerOffline = Controller ist nicht erreichbar. Konfiguration prüfen.

View File

@@ -0,0 +1,152 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="nanoleaf"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<bridge-type id="controller">
<label>@text/thing-type.nanoleaf.controller.name</label>
<description>@text/thing-type.nanoleaf.controller.description</description>
<channels>
<channel id="power" typeId="power"/>
<channel id="color" typeId="color"/>
<channel id="colorTemperature" typeId="colorTemperature"/>
<channel id="colorTemperatureAbs" typeId="colorTemperatureAbs"/>
<channel id="colorMode" typeId="colorMode"/>
<channel id="effect" typeId="effect"/>
<channel id="rhythmState" typeId="rhythmState"/>
<channel id="rhythmActive" typeId="rhythmActive"/>
<channel id="rhythmMode" typeId="rhythmMode"/>
<channel id="panelLayout" typeId="panelLayout"/>
</channels>
<properties>
<property name="vendor"/>
<property name="serialNumber"/>
<property name="firmwareVersion"/>
<property name="modelId"/>
</properties>
<representation-property>address</representation-property>
<config-description-ref uri="thing-type:nanoleaf:controller"/>
</bridge-type>
<thing-type id="lightpanel">
<supported-bridge-type-refs>
<bridge-type-ref id="controller"/>
</supported-bridge-type-refs>
<label>@text/thing-type.nanoleaf.lightpanel.name</label>
<description>@text/thing-type.nanoleaf.lightpanel.description</description>
<channels>
<channel id="panelColor" typeId="color"/>
<channel id="singleTap" typeId="singleTap"/>
<channel id="doubleTap" typeId="doubleTap"/>
</channels>
<representation-property>id</representation-property>
<config-description-ref uri="thing-type:nanoleaf:lightpanel"/>
</thing-type>
<channel-type id="power">
<item-type>Switch</item-type>
<label>@text/channel-type.nanoleaf.power.label</label>
<description>@text/channel-type.nanoleaf.power.description</description>
<category>Switch</category>
<state readOnly="false">
<options>
<option value="ON">On</option>
<option value="OFF">Off</option>
</options>
</state>
</channel-type>
<channel-type id="colorTemperature">
<item-type>Dimmer</item-type>
<label>@text/channel-type.nanoleaf.colorTemperature.label</label>
<description>@text/channel-type.nanoleaf.colorTemperature.description</description>
<state min="0" max="100" step="1"/>
</channel-type>
<channel-type id="colorTemperatureAbs">
<item-type>Number</item-type>
<label>@text/channel-type.nanoleaf.colorTemperatureAbs.label</label>
<description>@text/channel-type.nanoleaf.colorTemperatureAbs.description</description>
<category>ColorLight</category>
<state min="1200" max="6500" step="100"/>
</channel-type>
<channel-type id="colorMode">
<item-type>String</item-type>
<label>@text/channel-type.nanoleaf.colorMode.label</label>
<description>@text/channel-type.nanoleaf.colorMode.description</description>
<state readOnly="true">
<options>
<option value="effect">Effect mode</option>
<option value="hs">Hue/Saturation</option>
<option value="ct">Color temperature</option>
</options>
</state>
</channel-type>
<channel-type id="color">
<item-type>Color</item-type>
<label>@text/channel-type.nanoleaf.color.label</label>
<description>@text/Color of the Light Panels</description>
</channel-type>
<channel-type id="effect">
<item-type>String</item-type>
<label>@text/channel-type.nanoleaf.effect.label</label>
<description>@text/channel-type.nanoleaf.effect.description</description>
</channel-type>
<channel-type id="rhythmState">
<item-type>Switch</item-type>
<label>@text/channel-type.nanoleaf.rhythmState.label</label>
<description>@text/channel-type.nanoleaf.rhythmState.description</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="rhythmActive">
<item-type>Switch</item-type>
<label>@text/channel-type.nanoleaf.rhythmActive.label</label>
<description>@text/channel-type.nanoleaf.rhythmActive.description</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="rhythmMode">
<item-type>Number</item-type>
<label>@text/channel-type.nanoleaf.rhythmMode.label</label>
<description>@text/channel-type.nanoleaf.rhythmMode.description</description>
<state min="0" max="1">
<options>
<option value="0">Microphone</option>
<option value="1">Aux cable</option>
</options>
</state>
</channel-type>
<channel-type id="singleTap">
<item-type>Switch</item-type>
<label>@text/channel-type.nanoleaf.singleTap.label</label>
<description>@text/channel-type.nanoleaf.singleTap.description</description>
<state readOnly="false"/>
</channel-type>
<channel-type id="doubleTap">
<item-type>Switch</item-type>
<label>@text/channel-type.nanoleaf.doubleTap.label</label>
<description>@text/channel-type.nanoleaf.doubleTap.description</description>
<state readOnly="false"/>
</channel-type>
<channel-type id="panelLayout">
<item-type>Switch</item-type>
<label>@text/channel-type.nanoleaf.panelLayout.label</label>
<description>@text/channel-type.nanoleaf.panelLayout.description</description>
<state readOnly="false"/>
</channel-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,75 @@
/**
* Copyright (c) 2010-2020 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;
import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.assertThat;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.junit.Before;
import org.junit.Test;
import org.openhab.binding.nanoleaf.internal.model.Layout;
import com.google.gson.Gson;
/**
* Test for the Layout
*
* @author Stefan Höhn - Initial contribution
*/
@NonNullByDefault
public class LayoutTest {
private final Gson gson = new Gson();
String layout1Json = "";
String layoutInconsistentPanelNoJson = "";
@Before
public void setup() {
layout1Json = "{\"numPanels\":14,\"sideLength\":100,\"positionData\":[{\"panelId\":41451,\"x\":350,\"y\":0,\"o\":0,\"shapeType\":3},{\"panelId\":8134,\"x\":350,\"y\":150,\"o\":0,\"shapeType\":2},{\"panelId\":58086,\"x\":200,\"y\":100,\"o\":270,\"shapeType\":2},{\"panelId\":38724,\"x\":300,\"y\":200,\"o\":0,\"shapeType\":2},{\"panelId\":48111,\"x\":200,\"y\":200,\"o\":270,\"shapeType\":2},{\"panelId\":56093,\"x\":100,\"y\":200,\"o\":0,\"shapeType\":2},{\"panelId\":55836,\"x\":0,\"y\":200,\"o\":0,\"shapeType\":2},{\"panelId\":31413,\"x\":100,\"y\":300,\"o\":90,\"shapeType\":2},{\"panelId\":9162,\"x\":300,\"y\":300,\"o\":90,\"shapeType\":2},{\"panelId\":13276,\"x\":400,\"y\":300,\"o\":90,\"shapeType\":2},{\"panelId\":17870,\"x\":400,\"y\":200,\"o\":0,\"shapeType\":2},{\"panelId\":5164,\"x\":500,\"y\":200,\"o\":0,\"shapeType\":2},{\"panelId\":64279,\"x\":600,\"y\":200,\"o\":0,\"shapeType\":2},{\"panelId\":39755,\"x\":500,\"y\":100,\"o\":90,\"shapeType\":2}]}";
// panel number is not consistent to returned panels in array but it should still work
layoutInconsistentPanelNoJson = "{\"numPanels\":15,\"sideLength\":100,\"positionData\":[{\"panelId\":41451,\"x\":350,\"y\":0,\"o\":0,\"shapeType\":3},{\"panelId\":8134,\"x\":350,\"y\":150,\"o\":0,\"shapeType\":2},{\"panelId\":58086,\"x\":200,\"y\":100,\"o\":270,\"shapeType\":2},{\"panelId\":38724,\"x\":300,\"y\":200,\"o\":0,\"shapeType\":2},{\"panelId\":48111,\"x\":200,\"y\":200,\"o\":270,\"shapeType\":2},{\"panelId\":56093,\"x\":100,\"y\":200,\"o\":0,\"shapeType\":2},{\"panelId\":55836,\"x\":0,\"y\":200,\"o\":0,\"shapeType\":2},{\"panelId\":31413,\"x\":100,\"y\":300,\"o\":90,\"shapeType\":2},{\"panelId\":9162,\"x\":300,\"y\":300,\"o\":90,\"shapeType\":2},{\"panelId\":13276,\"x\":400,\"y\":300,\"o\":90,\"shapeType\":2},{\"panelId\":17870,\"x\":400,\"y\":200,\"o\":0,\"shapeType\":2},{\"panelId\":5164,\"x\":500,\"y\":200,\"o\":0,\"shapeType\":2},{\"panelId\":64279,\"x\":600,\"y\":200,\"o\":0,\"shapeType\":2},{\"panelId\":39755,\"x\":500,\"y\":100,\"o\":90,\"shapeType\":2}]}";
}
@Test
public void testTheRightLayoutView() {
@Nullable
Layout layout = gson.fromJson(layout1Json, Layout.class);
String layoutView = layout.getLayoutView();
assertThat(layoutView,
is(equalTo(" 31413 9162 13276 \n"
+ " \n"
+ "55836 56093 48111 38724 17870 5164 64279 \n"
+ " 8134 \n"
+ " 58086 39755 \n"
+ " \n"
+ " 41451 \n")));
}
@Test
public void testTheInconsistentLayoutView() {
@Nullable
Layout layout = gson.fromJson(layoutInconsistentPanelNoJson, Layout.class);
String layoutView = layout.getLayoutView();
assertThat(layoutView,
is(equalTo(" 31413 9162 13276 \n"
+ " \n"
+ "55836 56093 48111 38724 17870 5164 64279 \n"
+ " 8134 \n"
+ " 58086 39755 \n"
+ " \n"
+ " 41451 \n")));
}
}

View File

@@ -0,0 +1,50 @@
/**
* Copyright (c) 2010-2020 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;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.assertThat;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.junit.Test;
import org.openhab.binding.nanoleaf.internal.model.TouchEvent;
import org.openhab.binding.nanoleaf.internal.model.TouchEvents;
import com.google.gson.Gson;
/**
* Test for the TouchEvents
*
* @author Stefan Höhn - Initial contribution
*/
@NonNullByDefault
public class TouchTest {
private final Gson gson = new Gson();
@Test
public void testTheRightLayoutView() {
String json = "{\"events\":[{\"panelId\":48111,\"gesture\":1}]}";
@Nullable
TouchEvents touchEvents = gson.fromJson(json, TouchEvents.class);
assertThat(touchEvents.getEvents().size(), greaterThan(0));
assertThat(touchEvents.getEvents().size(), is(1));
@Nullable
TouchEvent touchEvent = touchEvents.getEvents().get(0);
assertThat(touchEvent.getPanelId(), is("48111"));
assertThat(touchEvent.getGesture(), is(1));
}
}

View File

@@ -0,0 +1,80 @@
/**
* Copyright (c) 2010-2020 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.handler;
import static java.nio.file.Files.*;
import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.assertThat;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.Before;
import org.junit.Test;
import org.openhab.binding.nanoleaf.internal.model.ControllerInfo;
import org.openhab.binding.nanoleaf.internal.model.State;
import org.openhab.core.library.types.OnOffType;
import com.google.gson.Gson;
/**
* Test for the Layout
*
* @author Stefan Höhn - Initial contribution
*/
@NonNullByDefault
public class NanoleafControllerHandlerTest {
private final Gson gson = new Gson();
private String controllerInfoJSON = "";
@Before
public void setup() {
}
@Test
public void testStateOn() {
controllerInfoJSON = "{\r\n \"name\":\"Nanoleaf Light Panels 12:34:56\",\r\n \"serialNo\":\"S19082ABCDE\",\r\n \"manufacturer\":\"Nanoleaf\",\r\n \"firmwareVersion\":\"3.3.3\",\r\n \"hardwareVersion\":\"1.6-2\",\r\n \"model\":\"NL22\",\r\n \"cloudHash\":{\r\n\r\n },\r\n \"discovery\":{\r\n\r\n },\r\n \"effects\":{\r\n \"effectsList\":[\r\n \"Color Burst\",\r\n \"Fireworks\",\r\n \"Flames\",\r\n \"Forest\",\r\n \"Inner Peace\",\r\n \"Lightning\",\r\n \"Northern Lights\",\r\n \"Pulse Pop Beats\",\r\n \"Vibrant Sunrise\"\r\n ],\r\n \"select\":\"Flames\"\r\n },\r\n \"firmwareUpgrade\":{\r\n\r\n },\r\n \"panelLayout\":{\r\n \"globalOrientation\":{\r\n \"value\":0,\r\n \"max\":360,\r\n \"min\":0\r\n },\r\n \"layout\":{\r\n \"numPanels\":9,\r\n \"sideLength\":150,\r\n \"positionData\":[\r\n {\r\n \"panelId\":1,\r\n \"x\":299,\r\n \"y\":0,\r\n \"o\":300,\r\n \"shapeType\":0\r\n },\r\n {\r\n \"panelId\":2,\r\n \"x\":299,\r\n \"y\":86,\r\n \"o\":120,\r\n \"shapeType\":0\r\n },\r\n {\r\n \"panelId\":3,\r\n \"x\":224,\r\n \"y\":129,\r\n \"o\":60,\r\n \"shapeType\":0\r\n },\r\n {\r\n \"panelId\":4,\r\n \"x\":224,\r\n \"y\":216,\r\n \"o\":120,\r\n \"shapeType\":0\r\n },\r\n {\r\n \"panelId\":5,\r\n \"x\":149,\r\n \"y\":259,\r\n \"o\":60,\r\n \"shapeType\":0\r\n },\r\n {\r\n \"panelId\":6,\r\n \"x\":74,\r\n \"y\":216,\r\n \"o\":240,\r\n \"shapeType\":0\r\n },\r\n {\r\n \"panelId\":7,\r\n \"x\":0,\r\n \"y\":259,\r\n \"o\":60,\r\n \"shapeType\":0\r\n },\r\n {\r\n \"panelId\":8,\r\n \"x\":149,\r\n \"y\":346,\r\n \"o\":120,\r\n \"shapeType\":0\r\n },\r\n {\r\n \"panelId\":9,\r\n \"x\":374,\r\n \"y\":129,\r\n \"o\":180,\r\n \"shapeType\":0\r\n }\r\n ]\r\n }\r\n },\r\n \"rhythm\":{\r\n \"auxAvailable\":false,\r\n \"firmwareVersion\":\"2.4.3\",\r\n \"hardwareVersion\":\"2.0\",\r\n \"rhythmActive\":false,\r\n \"rhythmConnected\":true,\r\n \"rhythmId\":10,\r\n \"rhythmMode\":0,\r\n \"rhythmPos\":{\r\n \"x\":449.99521692839559,\r\n \"y\":86.60030339609753,\r\n \"o\":0.0\r\n }\r\n },\r\n \"schedules\":{\r\n\r\n },\r\n \"state\":{\r\n \"brightness\":{\r\n \"value\":29,\r\n \"max\":100,\r\n \"min\":0\r\n },\r\n \"colorMode\":\"effect\",\r\n \"ct\":{\r\n \"value\":3000,\r\n \"max\":6500,\r\n \"min\":1200\r\n },\r\n \"hue\":{\r\n \"value\":0,\r\n \"max\":360,\r\n \"min\":0\r\n },\r\n \"on\":{\r\n \"value\":true\r\n },\r\n \"sat\":{\r\n \"value\":0,\r\n \"max\":100,\r\n \"min\":0\r\n }\r\n }\r\n}";
ControllerInfo controllerInfo = gson.fromJson(controllerInfoJSON, ControllerInfo.class);
final State state = controllerInfo.getState();
assertThat(state, is(notNullValue()));
assertThat(state.getOnOff(), is(OnOffType.ON));
}
@Test
public void testStateOff() {
controllerInfoJSON = "{\r\n \"name\":\"Nanoleaf Light Panels 12:34:56\",\r\n \"serialNo\":\"S19082ABCDE\",\r\n \"manufacturer\":\"Nanoleaf\",\r\n \"firmwareVersion\":\"3.3.3\",\r\n \"hardwareVersion\":\"1.6-2\",\r\n \"model\":\"NL22\",\r\n \"cloudHash\":{\r\n\r\n },\r\n \"discovery\":{\r\n\r\n },\r\n \"effects\":{\r\n \"effectsList\":[\r\n \"Color Burst\",\r\n \"Fireworks\",\r\n \"Flames\",\r\n \"Forest\",\r\n \"Inner Peace\",\r\n \"Lightning\",\r\n \"Northern Lights\",\r\n \"Pulse Pop Beats\",\r\n \"Vibrant Sunrise\"\r\n ],\r\n \"select\":\"Flames\"\r\n },\r\n \"firmwareUpgrade\":{\r\n\r\n },\r\n \"panelLayout\":{\r\n \"globalOrientation\":{\r\n \"value\":0,\r\n \"max\":360,\r\n \"min\":0\r\n },\r\n \"layout\":{\r\n \"numPanels\":9,\r\n \"sideLength\":150,\r\n \"positionData\":[\r\n {\r\n \"panelId\":1,\r\n \"x\":299,\r\n \"y\":0,\r\n \"o\":300,\r\n \"shapeType\":0\r\n },\r\n {\r\n \"panelId\":2,\r\n \"x\":299,\r\n \"y\":86,\r\n \"o\":120,\r\n \"shapeType\":0\r\n },\r\n {\r\n \"panelId\":3,\r\n \"x\":224,\r\n \"y\":129,\r\n \"o\":60,\r\n \"shapeType\":0\r\n },\r\n {\r\n \"panelId\":4,\r\n \"x\":224,\r\n \"y\":216,\r\n \"o\":120,\r\n \"shapeType\":0\r\n },\r\n {\r\n \"panelId\":5,\r\n \"x\":149,\r\n \"y\":259,\r\n \"o\":60,\r\n \"shapeType\":0\r\n },\r\n {\r\n \"panelId\":6,\r\n \"x\":74,\r\n \"y\":216,\r\n \"o\":240,\r\n \"shapeType\":0\r\n },\r\n {\r\n \"panelId\":7,\r\n \"x\":0,\r\n \"y\":259,\r\n \"o\":60,\r\n \"shapeType\":0\r\n },\r\n {\r\n \"panelId\":8,\r\n \"x\":149,\r\n \"y\":346,\r\n \"o\":120,\r\n \"shapeType\":0\r\n },\r\n {\r\n \"panelId\":9,\r\n \"x\":374,\r\n \"y\":129,\r\n \"o\":180,\r\n \"shapeType\":0\r\n }\r\n ]\r\n }\r\n },\r\n \"rhythm\":{\r\n \"auxAvailable\":false,\r\n \"firmwareVersion\":\"2.4.3\",\r\n \"hardwareVersion\":\"2.0\",\r\n \"rhythmActive\":false,\r\n \"rhythmConnected\":true,\r\n \"rhythmId\":10,\r\n \"rhythmMode\":0,\r\n \"rhythmPos\":{\r\n \"x\":449.99521692839559,\r\n \"y\":86.60030339609753,\r\n \"o\":0.0\r\n }\r\n },\r\n \"schedules\":{\r\n\r\n },\r\n \"state\":{\r\n \"brightness\":{\r\n \"value\":29,\r\n \"max\":100,\r\n \"min\":0\r\n },\r\n \"colorMode\":\"effect\",\r\n \"ct\":{\r\n \"value\":3000,\r\n \"max\":6500,\r\n \"min\":1200\r\n },\r\n \"hue\":{\r\n \"value\":0,\r\n \"max\":360,\r\n \"min\":0\r\n },\r\n \"sat\":{\r\n \"value\":0,\r\n \"max\":100,\r\n \"min\":0\r\n }\r\n }\r\n}";
ControllerInfo controllerInfo = gson.fromJson(controllerInfoJSON, ControllerInfo.class);
final State state = controllerInfo.getState();
assertThat(state, is(notNullValue()));
assertThat(state.getOnOff(), is(OnOffType.OFF));
}
@Test
public void testStateOnMissing() {
controllerInfoJSON = "{\r\n \"name\":\"Nanoleaf Light Panels 12:34:56\",\r\n \"serialNo\":\"S19082ABCDE\",\r\n \"manufacturer\":\"Nanoleaf\",\r\n \"firmwareVersion\":\"3.3.3\",\r\n \"hardwareVersion\":\"1.6-2\",\r\n \"model\":\"NL22\",\r\n \"cloudHash\":{\r\n\r\n },\r\n \"discovery\":{\r\n\r\n },\r\n \"effects\":{\r\n \"effectsList\":[\r\n \"Color Burst\",\r\n \"Fireworks\",\r\n \"Flames\",\r\n \"Forest\",\r\n \"Inner Peace\",\r\n \"Lightning\",\r\n \"Northern Lights\",\r\n \"Pulse Pop Beats\",\r\n \"Vibrant Sunrise\"\r\n ],\r\n \"select\":\"Flames\"\r\n },\r\n \"firmwareUpgrade\":{\r\n\r\n },\r\n \"panelLayout\":{\r\n \"globalOrientation\":{\r\n \"value\":0,\r\n \"max\":360,\r\n \"min\":0\r\n },\r\n \"layout\":{\r\n \"numPanels\":9,\r\n \"sideLength\":150,\r\n \"positionData\":[\r\n {\r\n \"panelId\":1,\r\n \"x\":299,\r\n \"y\":0,\r\n \"o\":300,\r\n \"shapeType\":0\r\n },\r\n {\r\n \"panelId\":2,\r\n \"x\":299,\r\n \"y\":86,\r\n \"o\":120,\r\n \"shapeType\":0\r\n },\r\n {\r\n \"panelId\":3,\r\n \"x\":224,\r\n \"y\":129,\r\n \"o\":60,\r\n \"shapeType\":0\r\n },\r\n {\r\n \"panelId\":4,\r\n \"x\":224,\r\n \"y\":216,\r\n \"o\":120,\r\n \"shapeType\":0\r\n },\r\n {\r\n \"panelId\":5,\r\n \"x\":149,\r\n \"y\":259,\r\n \"o\":60,\r\n \"shapeType\":0\r\n },\r\n {\r\n \"panelId\":6,\r\n \"x\":74,\r\n \"y\":216,\r\n \"o\":240,\r\n \"shapeType\":0\r\n },\r\n {\r\n \"panelId\":7,\r\n \"x\":0,\r\n \"y\":259,\r\n \"o\":60,\r\n \"shapeType\":0\r\n },\r\n {\r\n \"panelId\":8,\r\n \"x\":149,\r\n \"y\":346,\r\n \"o\":120,\r\n \"shapeType\":0\r\n },\r\n {\r\n \"panelId\":9,\r\n \"x\":374,\r\n \"y\":129,\r\n \"o\":180,\r\n \"shapeType\":0\r\n }\r\n ]\r\n }\r\n },\r\n \"rhythm\":{\r\n \"auxAvailable\":false,\r\n \"firmwareVersion\":\"2.4.3\",\r\n \"hardwareVersion\":\"2.0\",\r\n \"rhythmActive\":false,\r\n \"rhythmConnected\":true,\r\n \"rhythmId\":10,\r\n \"rhythmMode\":0,\r\n \"rhythmPos\":{\r\n \"x\":449.99521692839559,\r\n \"y\":86.60030339609753,\r\n \"o\":0.0\r\n }\r\n },\r\n \"schedules\":{\r\n\r\n },\r\n \"state\":{\r\n \"brightness\":{\r\n \"value\":29,\r\n \"max\":100,\r\n \"min\":0\r\n },\r\n \"colorMode\":\"effect\",\r\n \"ct\":{\r\n \"value\":3000,\r\n \"max\":6500,\r\n \"min\":1200\r\n },\r\n \"hue\":{\r\n \"value\":0,\r\n \"max\":360,\r\n \"min\":0\r\n },\r\n \"on\":{\r\n \"value\":false\r\n },\r\n \"sat\":{\r\n \"value\":0,\r\n \"max\":100,\r\n \"min\":0\r\n }\r\n }\r\n}";
ControllerInfo controllerInfo = gson.fromJson(controllerInfoJSON, ControllerInfo.class);
final State state = controllerInfo.getState();
assertThat(state, is(notNullValue()));
assertThat(state.getOnOff(), is(OnOffType.OFF));
}
}