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.tradfri</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,124 @@
# TRÅDFRI Binding
This binding integrates the IKEA TRÅDFRI gateway and devices connected to it (such as dimmable LED bulbs).
## Supported Things
Beside the gateway (thing type "gateway"), the binding currently supports colored bulbs, dimmable warm white bulbs as well as white spectrum bulbs and control outlets.
The binding also supports read-only data from remote controls and motion sensors (e.g. the battery status).
The TRÅDFRI controller and sensor devices currently cannot be observed right away because they are communicating directly with the bulbs or lamps without routing their commands through the gateway.
This makes it nearly impossible to trigger events for pressed buttons.
We only can access some static data like the present status or battery level.
The thing type ids are defined according to the lighting devices defined for ZigBee LightLink ([see page 24, table 2](https://www.nxp.com/documents/user_manual/JN-UG-3091.pdf).
These are:
| Device type | ZigBee Device ID | Thing type |
|---------------------------------|------------------|------------|
| Dimmable Light | 0x0100 | 0100 |
| Colour Temperature Light | 0x0220 | 0220 |
| Extended Colour Light | 0x0210 | 0210 |
| Occupancy Sensor | 0x0107 | 0107 |
| Non-Colour Controller | 0x0820 | 0820 |
| Non-Colour Scene Controller | 0x0830 | 0830 |
| Control Outlet | 0x0010 | 0010 |
| Window Covering Device | 0x0202 | 0202 |
| Window Covering Controller | 0x0202 | 0203 |
The following matrix lists the capabilities (channels) for each of the supported lighting device types:
| Thing type | Brightness | Color | Color Temperature | Battery Level | Battery Low | Power | Position |
|-------------|:----------:|:-----:|:-----------------:|:-------------:|:-----------:|:-----:|:---------|
| 0010 | | | | | | X | |
| 0100 | X | | | | | | |
| 0220 | X | | X | | | | |
| 0210 | | X | X | | | | |
| 0107 | | | | X | X | | |
| 0820 | | | | X | X | | |
| 0830 | | | | X | X | | |
| 0202 | | | | X | X | | X |
| 0203 | | | | X | X | | |
## Thing Configuration
For first pairing - the gateway requires a `host` parameter for the hostname or IP address and a `code`, which is the security code that is printed on the bottom of the gateway.
Optionally, a `port` can be configured, but any standard gateway uses the default port 5684.
The gateway requires at least firmware version 1.2.42 to connect to this binding.
The `code` is used during the initialization for retrieving unique identity and pre-shared key from the gateway and then it is discarded from the configuration.
The newly created authentication data is stored in advanced parameters `identity` and `preSharedKey`.
On each initialization if the code is present in the thing configuration - the `identity` and `preSharedKey` are recreated and the `code` is again discarded.
The devices require only a single (integer) parameter, which is their instance id. Unfortunately, this is not displayed anywhere in the IKEA app, but it seems that they are sequentially numbered starting with 65537 for the first device. If in doubt, use the auto-discovered things to find out the correct instance ids.
## Channels
The dimmable bulbs support the `brightness` channel.
The white spectrum bulbs additionally also support the `color_temperature` channel.
Full color bulbs support the `color_temperature` and `color` channels.
Brightness can be changed with the `color` channel.
The remote control and the motion sensor supports the `battery_level` and `battery_low` channels for reading the battery status.
The control outlet supports the `power` channel.
A blind or curtain supports beside `battery_level` and `battery_low` channels a `positon` channel.
Refer to the matrix above.
| Channel Type ID | Item Type | Description |
|-------------------|---------------|--------------------------------------------------------|
| brightness | Dimmer | The brightness of the bulb in percent |
| color_temperature | Dimmer | color temperature from 0% = cold to 100% = warm |
| color | Color | full color |
| battery_level | Number | battery level (in %) |
| battery_low | Switch | battery low warning (<=10% = ON, >10% = OFF) |
| power | Switch | power switch |
| position | Rollershutter | position of the blinds from 0% = open to 100% = closed |
## Full Example
demo.things:
```
Bridge tradfri:gateway:mygateway [ host="192.168.0.177", code="EHPW5rIJKyXFgjH3" ] {
0100 myDimmableBulb "My Dimmable Bulb" [ id=65537 ]
0220 myColorTempBulb "My Color Temp Bulb" [ id=65538 ]
0210 myColorBulb "My Color Bulb" [ id=65539 ]
0830 myRemoteControl "My Remote Control" [ id=65545 ]
0010 myControlOutlet "My Control Outlet" [ id=65542 ]
0202 myBlinds "My Blinds" [ id=65547 ]
}
```
demo.items:
```
Dimmer Light1 { channel="tradfri:0100:mygateway:myDimmableBulb:brightness" }
Dimmer Light2_Brightness { channel="tradfri:0220:mygateway:myColorTempBulb:brightness" }
Dimmer Light2_ColorTemperature { channel="tradfri:0220:mygateway:myColorTempBulb:color_temperature" }
Color ColorLight { channel="tradfri:0210:mygateway:myColorBulb:color" }
Number RemoteControlBatteryLevel { channel="tradfri:0830:mygateway:myRemoteControl:battery_level" }
Switch RemoteControlBatteryLow { channel="tradfri:0830:mygateway:myRemoteControl:battery_low" }
Switch ControlOutlet { channel="tradfri:0010:mygateway:myControlOutlet:power" }
Rollershutter BlindPosition { channel="tradfri:0202:mygateway:myBlinds:position" }
```
demo.sitemap:
```
sitemap demo label="Main Menu"
{
Frame {
Slider item=Light1 label="Light1 Brightness [%.1f %%]"
Slider item=Light2_Brightness label="Light2 Brightness [%.1f %%]"
Slider item=Light2_ColorTemperature label="Light2 Color Temperature [%.1f %%]"
Colorpicker item=ColorLight label="Color"
Text item=RemoteControlBatteryLevel label="Battery Level [%d %%]"
Switch item=RemoteControlBatteryLow label="Battery Low Warning"
Switch item=ControlOutlet label="Power Switch"
Switch item=BlindPosition label="Blind Position [%d]"
}
}
```

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.tradfri</artifactId>
<name>openHAB Add-ons :: Bundles :: TRÅDFRI Binding</name>
</project>

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<features name="org.openhab.binding.tradfri-${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-tradfri" description="TRÅDFRI Binding" version="${project.version}">
<feature>openhab-runtime-base</feature>
<feature>openhab-transport-coap</feature>
<feature>openhab-transport-mdns</feature>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.tradfri/${project.version}</bundle>
</feature>
</features>

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.tradfri.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import com.google.gson.JsonElement;
/**
* The {@link CoapCallback} is receives coap response data asynchronously.
*
* @author Kai Kreuzer - Initial contribution
*/
@NonNullByDefault
public interface CoapCallback {
/**
* This is being called, if new data is received from a CoAP request.
*
* @param data the received json structure
*/
public void onUpdate(JsonElement data);
/**
* Tells the listener to set the Thing status.
* Should usually be directly passed on to updateStatus() on the ThingHandler.
*
* @param status The thing status
* @param statusDetail the status detail
*/
public void setStatus(ThingStatus status, ThingStatusDetail statusDetail);
}

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.tradfri.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.tradfri.internal.handler.TradfriGatewayHandler;
import com.google.gson.JsonObject;
/**
* {@link DeviceUpdateListener} can register on the {@link TradfriGatewayHandler} to be
* informed about details about devices.
*
* @author Kai Kreuzer - Initial contribution
*/
@NonNullByDefault
public interface DeviceUpdateListener {
/**
* This method is called when new device information is received.
*
* @param instanceId The instance id of the device
* @param data the json data describing the device
*/
public void onUpdate(String instanceId, JsonObject data);
}

View File

@@ -0,0 +1,208 @@
/**
* 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.tradfri.internal;
import java.util.Collections;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.ThingTypeUID;
/**
* The {@link TradfriBindingConstants} class defines common constants, which are
* used across the whole binding.
*
* @author Kai Kreuzer - Initial contribution
* @author Christoph Weitkamp - Added support for remote controller and motion sensor devices (read-only battery level)
* @author Manuel Raffel - Added support for blinds
*/
@NonNullByDefault
public class TradfriBindingConstants {
private static final String BINDING_ID = "tradfri";
// List of all Thing Type UIDs
public static final ThingTypeUID GATEWAY_TYPE_UID = new ThingTypeUID(BINDING_ID, "gateway");
public static final ThingTypeUID THING_TYPE_ONOFF_PLUG = new ThingTypeUID(BINDING_ID, "0010");
public static final ThingTypeUID THING_TYPE_DIMMABLE_LIGHT = new ThingTypeUID(BINDING_ID, "0100");
public static final ThingTypeUID THING_TYPE_COLOR_TEMP_LIGHT = new ThingTypeUID(BINDING_ID, "0220");
public static final ThingTypeUID THING_TYPE_COLOR_LIGHT = new ThingTypeUID(BINDING_ID, "0210");
public static final ThingTypeUID THING_TYPE_DIMMER = new ThingTypeUID(BINDING_ID, "0820");
public static final ThingTypeUID THING_TYPE_REMOTE_CONTROL = new ThingTypeUID(BINDING_ID, "0830");
public static final ThingTypeUID THING_TYPE_MOTION_SENSOR = new ThingTypeUID(BINDING_ID, "0107");
public static final ThingTypeUID THING_TYPE_BLINDS = new ThingTypeUID(BINDING_ID, "0202");
public static final ThingTypeUID THING_TYPE_OPEN_CLOSE_REMOTE_CONTROL = new ThingTypeUID(BINDING_ID, "0203");
public static final Set<ThingTypeUID> SUPPORTED_LIGHT_TYPES_UIDS = Collections
.unmodifiableSet(Stream.of(THING_TYPE_DIMMABLE_LIGHT, THING_TYPE_COLOR_TEMP_LIGHT, THING_TYPE_COLOR_LIGHT)
.collect(Collectors.toSet()));
public static final Set<ThingTypeUID> SUPPORTED_PLUG_TYPES_UIDS = Collections.singleton(THING_TYPE_ONOFF_PLUG);
public static final Set<ThingTypeUID> SUPPORTED_BLINDS_TYPES_UIDS = Collections.singleton(THING_TYPE_BLINDS);
// List of all Gateway Configuration Properties
public static final String GATEWAY_CONFIG_HOST = "host";
public static final String GATEWAY_CONFIG_PORT = "port";
public static final String GATEWAY_CONFIG_CODE = "code";
public static final String GATEWAY_CONFIG_IDENTITY = "identity";
public static final String GATEWAY_CONFIG_PRE_SHARED_KEY = "preSharedKey";
// Not yet used - included for future support
public static final Set<ThingTypeUID> SUPPORTED_CONTROLLER_TYPES_UIDS = Collections
.unmodifiableSet(Stream.of(THING_TYPE_DIMMER, THING_TYPE_REMOTE_CONTROL,
THING_TYPE_OPEN_CLOSE_REMOTE_CONTROL, THING_TYPE_MOTION_SENSOR).collect(Collectors.toSet()));
public static final Set<ThingTypeUID> SUPPORTED_BRIDGE_TYPES_UIDS = Collections.singleton(GATEWAY_TYPE_UID);
public static final Set<ThingTypeUID> SUPPORTED_DEVICE_TYPES_UIDS = Collections.unmodifiableSet(Stream
.of(SUPPORTED_LIGHT_TYPES_UIDS.stream(), SUPPORTED_CONTROLLER_TYPES_UIDS.stream(),
SUPPORTED_PLUG_TYPES_UIDS.stream(), SUPPORTED_BLINDS_TYPES_UIDS.stream())
.reduce(Stream::concat).orElseGet(Stream::empty).collect(Collectors.toSet()));
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Collections
.unmodifiableSet(Stream.concat(SUPPORTED_BRIDGE_TYPES_UIDS.stream(), SUPPORTED_DEVICE_TYPES_UIDS.stream())
.collect(Collectors.toSet()));
// List of all Channel IDs
public static final String CHANNEL_POWER = "power";
public static final String CHANNEL_BRIGHTNESS = "brightness";
public static final String CHANNEL_COLOR_TEMPERATURE = "color_temperature";
public static final String CHANNEL_COLOR = "color";
public static final String CHANNEL_POSITION = "position";
public static final String CHANNEL_BATTERY_LEVEL = "battery_level";
public static final String CHANNEL_BATTERY_LOW = "battery_low";
// IPSO Objects
public static final String DEVICES = "15001";
public static final String AUTH_PATH = "9063";
public static final String BLINDS = "15015";
public static final String CLIENT_IDENTITY_PROPOSED = "9090";
public static final String COLOR = "5706";
public static final String COLOR_X = "5709";
public static final String COLOR_Y = "5710";
public static final String COMMISSIONING_MODE = "9061";
public static final String CREATED_AT = "9002";
public static final String CUM_ACTIVE_POWER = "5805";
public static final String CURRENT_TIMESTAMP = "9059";
public static final int DEFAULT_DIMMER_TRANSITION_TIME = 5;
public static final String DEVICE = "3";
public static final String DIMMER = "5851";
public static final int DIMMER_MAX = 254;
public static final int DIMMER_MIN = 0;
public static final String END_ACTION = "9043";
public static final String END_TIME_HR = "9048";
public static final String END_TIME_MN = "9049";
public static final String ERROR_TAG = "errorcode";
public static final String FORCE_CHECK_OTA_UPDATE = "9032";
public static final String GATEWAY = "15011";
public static final String GATEWAY_DETAILS = "15012";
public static final String GATEWAY_NAME = "9035";
public static final int GATEWAY_REBOOT_NOTIFICATION = 1003;
public static final String GATEWAY_REBOOT_NOTIFICATION_TYPE = "9052";
public static final String GATEWAY_TIME_SOURCE = "9071";
public static final String GATEWAY_UPDATE_DETAILS_URL = "9056";
public static final String GATEWAY_UPDATE_PROGRESS = "9055";
public static final String GROUPS = "15004";
public static final String GROUP_ID = "9038";
public static final String GROUP_LINK_ARRAY = "9995";
public static final String GROUP_SETTINGS = "9045";
public static final String HS_ACCESSORY_LINK = "9018";
public static final String HS_LINK = "15002";
public static final String IKEA_MOODS = "9068";
public static final String INSTANCE_ID = "9003";
public static final String LAST_SEEN = "9020";
public static final String LIGHT = "3311";
public static final int LIGHTS_OFF_SMART_TASK = 2;
public static final String LIGHT_SETTING = "15013";
public static final int LOSS_OF_INTERNET_CONNECTIVITY = 5001;
public static final String MASTER_TOKEN_TAG = "9036";
public static final String MAX_MSR_VALUE = "5602";
public static final String MAX_RNG_VALUE = "5604";
public static final String MIN_MSR_VALUE = "5601";
public static final String MIN_RNG_VALUE = "5603";
public static final String NAME = "9001";
public static final int NEW_FIRMWARE_AVAILABLE = 1001;
public static final String NEW_PSK_BY_GW = "9091";
public static final String NOTIFICATION_EVENT = "9015";
public static final String NOTIFICATION_NVPAIR = "9017";
public static final String NOTIFICATION_STATE = "9014";
public static final int NOT_AT_HOME_SMART_TASK = 1;
public static final String NTP_SERVER = "9023";
public static final String ONOFF = "5850";
public static final String ON_TIME = "5852";
public static final String OPEN = "1";
public static final int OPTION_APP_TOKEN = 2051;
public static final int OTA_CRITICAL = 1;
public static final int OTA_FORCED = 5;
public static final int OTA_NORMAL = 0;
public static final int OTA_REQUIRED = 2;
public static final String OTA_TYPE = "9066";
public static final String OTA_UPDATE = "9037";
public static final String OTA_UPDATE_STATE = "9054";
public static final String PLUG = "3312";
public static final String POWER_FACTOR = "5820";
public static final String POSITION = "5536";
public static final String REACHABILITY_STATE = "9019";
public static final String REBOOT = "9030";
public static final String REPEAT_DAYS = "9041";
public static final String REPEATER = "15014";
public static final String RESET = "9031";
public static final String RESET_MIN_MAX_MSR = "5605";
public static final String SCENE = "15005";
public static final String SCENE_ACTIVATE_FLAG = "9058";
public static final String SCENE_ID = "9039";
public static final String SCENE_INDEX = "9057";
public static final String SCENE_LINK = "9009";
public static final String SENSOR = "3300";
public static final String SENSOR_TYPE = "5751";
public static final String SENSOR_VALUE = "5700";
public static final String SESSION_ID = "9033";
public static final String SESSION_LENGTH = "9064";
public static final String SHORTCUT_ICON_REFERENCE_TYPE = "9051";
public static final String SMART_TASK_ACTION = "9050";
public static final String SMART_TASK_TEMPLATE = "9016";
public static final int SMART_TASK_TRIGGERED_EVENT = 1002;
public static final String SMART_TASK_TYPE = "9040";
public static final String START_ACTION = "9042";
public static final String START_TIME_HR = "9046";
public static final String START_TIME_MN = "9047";
public static final String STOP_TRIGGER = "5523";
public static final String SWITCH = "15009";
public static final String TIME_ARRAY = "9994";
public static final String TIME_REMAINING_IN_SECONDS = "9024";
public static final String TRANSITION_TIME = "5712";
public static final String TRIGGER_TIME_INTERVAL = "9044";
public static final String TYPE = "5750";
public static final String UNIT = "5701";
public static final String UPDATE_ACCEPTED_TIMESTAMP = "9069";
public static final String UPDATE_FIRMWARE = "9034";
public static final String USE_CURRENT_LIGHT_SETTINGS = "9070";
public static final String VERSION = "9029";
public static final int WAKE_UP_SMART_TASK = 3;
public static final String TYPE_SWITCH = "0";
public static final String TYPE_REMOTE = "1";
public static final String TYPE_LIGHT = "2";
public static final String TYPE_PLUG = "3";
public static final String TYPE_SENSOR = "4";
public static final String TYPE_REPEATER = "6";
public static final String TYPE_BLINDS = "7";
public static final String DEVICE_VENDOR = "0";
public static final String DEVICE_MODEL = "1";
public static final String DEVICE_FIRMWARE = "3";
public static final String DEVICE_BATTERY_LEVEL = "9";
}

View File

@@ -0,0 +1,149 @@
/**
* 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.tradfri.internal;
import java.net.URI;
import java.util.LinkedList;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import org.eclipse.californium.core.CoapClient;
import org.eclipse.californium.core.CoapObserveRelation;
import org.eclipse.californium.core.coap.MediaTypeRegistry;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link TradfriCoapClient} provides some convenience features over the
* plain {@link CoapClient} from californium.
*
* @author Kai Kreuzer - Initial contribution
*/
@NonNullByDefault
public class TradfriCoapClient extends CoapClient {
private static final long TIMEOUT = 2000;
private static final int DEFAULT_DELAY_MILLIS = 600;
private final Logger logger = LoggerFactory.getLogger(TradfriCoapClient.class);
private final LinkedList<PayloadCallbackPair> commandsQueue = new LinkedList<>();
private @Nullable Future<?> job;
public TradfriCoapClient(URI uri) {
super(uri);
setTimeout(TIMEOUT);
}
private void executeCommands() {
while (true) {
try {
synchronized (commandsQueue) {
if (commandsQueue.isEmpty()) {
return;
}
PayloadCallbackPair payloadCallbackPair = commandsQueue.poll();
logger.debug("CoAP PUT request\nuri: {}\npayload: {}", getURI(), payloadCallbackPair.payload);
put(new TradfriCoapHandler(payloadCallbackPair.callback), payloadCallbackPair.payload,
MediaTypeRegistry.TEXT_PLAIN);
}
Thread.sleep(DEFAULT_DELAY_MILLIS);
} catch (InterruptedException e) {
logger.debug("commandExecutorThread was interrupted", e);
}
}
}
/**
* Starts observation of the resource and uses the given callback to provide updates.
*
* @param callback the callback to use for updates
*/
public CoapObserveRelation startObserve(CoapCallback callback) {
return observe(new TradfriCoapHandler(callback));
}
/**
* Asynchronously executes a GET on the resource and provides the result through a {@link CompletableFuture}.
*
* @return the future that will hold the result
*/
public CompletableFuture<String> asyncGet() {
logger.debug("CoAP GET request\nuri: {}", getURI());
CompletableFuture<String> future = new CompletableFuture<>();
get(new TradfriCoapHandler(future));
return future;
}
/**
* Asynchronously executes a GET on the resource and provides the result to a given callback.
*
* @param callback the callback to use for the response
*/
public void asyncGet(CoapCallback callback) {
logger.debug("CoAP GET request\nuri: {}", getURI());
get(new TradfriCoapHandler(callback));
}
/**
* Asynchronously executes a PUT on the resource with a payload, provides the result to a given callback
* and blocks sending new requests to the resource for specified amount of milliseconds.
*
* @param payload the payload to send with the PUT request
* @param callback the callback to use for the response
* @param scheduler scheduler to be used for sending commands
*/
public void asyncPut(String payload, CoapCallback callback, ScheduledExecutorService scheduler) {
asyncPut(new PayloadCallbackPair(payload, callback), scheduler);
}
/**
* Asynchronously executes a PUT on the resource with a payload and provides the result to a given callback.
*
* @param payloadCallbackPair object which holds the payload and callback process the PUT request
* @param scheduler scheduler to be used for sending commands
*/
public void asyncPut(PayloadCallbackPair payloadCallbackPair, ScheduledExecutorService scheduler) {
synchronized (this.commandsQueue) {
if (this.commandsQueue.isEmpty()) {
this.commandsQueue.offer(payloadCallbackPair);
final Future<?> job = this.job;
if (job == null || job.isDone()) {
this.job = scheduler.submit(() -> executeCommands());
}
} else {
this.commandsQueue.offer(payloadCallbackPair);
}
}
}
@Override
public void shutdown() {
if (job != null) {
job.cancel(true);
}
super.shutdown();
}
public final class PayloadCallbackPair {
public final String payload;
public final CoapCallback callback;
public PayloadCallbackPair(String payload, CoapCallback callback) {
this.payload = payload;
this.callback = callback;
}
}
}

View File

@@ -0,0 +1,105 @@
/**
* 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.tradfri.internal;
import java.util.concurrent.CompletableFuture;
import org.eclipse.californium.core.CoapHandler;
import org.eclipse.californium.core.CoapResponse;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.JsonParseException;
import com.google.gson.JsonParser;
/**
* The {@link TradfriCoapHandler} is used to handle the asynchronous coap reponses.
* It can either be used with a callback class or with a future.
*
* @author Kai Kreuzer - Initial contribution
*/
@NonNullByDefault
public class TradfriCoapHandler implements CoapHandler {
private final Logger logger = LoggerFactory.getLogger(TradfriCoapHandler.class);
private final JsonParser parser = new JsonParser();
private @Nullable CoapCallback callback;
private @Nullable CompletableFuture<String> future;
/**
* Constructor for using a callback
*
* @param callback the callback to use for responses
*/
public TradfriCoapHandler(CoapCallback callback) {
this.callback = callback;
}
/**
* Constructor for using a future
*
* @param future the future to use for responses
*/
public TradfriCoapHandler(CompletableFuture<String> future) {
this.future = future;
}
@Override
public void onLoad(@Nullable CoapResponse response) {
if (response == null) {
logger.trace("received empty CoAP response");
return;
}
logger.debug("CoAP response\noptions: {}\npayload: {}", response.getOptions(), response.getResponseText());
if (response.isSuccess()) {
final CoapCallback callback = this.callback;
if (callback != null) {
try {
callback.onUpdate(parser.parse(response.getResponseText()));
callback.setStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE);
} catch (JsonParseException e) {
logger.warn("Observed value is no valid json: {}, {}", response.getResponseText(), e.getMessage());
}
}
final CompletableFuture<String> future = this.future;
if (future != null) {
String data = response.getResponseText();
future.complete(data);
}
} else {
logger.debug("CoAP error {}", response.getCode());
if (callback != null) {
callback.setStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
}
if (future != null) {
future.completeExceptionally(new RuntimeException("Response " + response.getCode().toString()));
}
}
}
@Override
public void onError() {
logger.debug("CoAP onError");
if (callback != null) {
callback.setStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
}
if (future != null) {
future.completeExceptionally(new RuntimeException("CoAP GET resulted in an error."));
}
}
}

View File

@@ -0,0 +1,188 @@
/**
* 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.tradfri.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.library.types.HSBType;
import org.openhab.core.library.types.PercentType;
/**
* The {@link TradfriColor} is used for conversion between color formats.
* Use the static methods {@link TradfriColor#fromCie(int, int, int)} and {@link TradfriColor#fromHSBType(HSBType)} for
* construction.
*
* @author Holger Reichert - Initial contribution
* @author Stefan Triller - Use conversions from HSBType
*
*/
@NonNullByDefault
public class TradfriColor {
// Tradfri uses the CIE color space (see https://en.wikipedia.org/wiki/CIE_1931_color_space),
// which uses x,y-coordinates.
// Its own app comes with 3 predefined color temperature settings (0,1,2), which have those values:
private static final double[] PRESET_X = new double[] { 24933.0, 30138.0, 33137.0 };
private static final double[] PRESET_Y = new double[] { 24691.0, 26909.0, 27211.0 };
/**
* CIE XY color values in the tradfri range 0 to 65535.
* May be <code>null</code> if the calculation method does not support this color range.
*/
public Integer xyX, xyY;
/**
* Brightness level in the tradfri range 0 to 254.
* May be <code>null</code> if the calculation method does not support this color range.
*/
public @Nullable Integer brightness;
/**
* Construct from CIE XY values in the tradfri range.
*
* @param xyX x value 0 to 65535
* @param xyY y value 0 to 65535
* @param xyBrightness brightness from 0 to 254
*/
public TradfriColor(Integer xyX, Integer xyY, @Nullable Integer brightness) {
this.xyX = xyX;
this.xyY = xyY;
if (brightness != null) {
if (brightness > 254) {
this.brightness = 254;
} else {
this.brightness = brightness;
}
}
}
/**
* Construct from HSBType
*
* @param hsb HSBType from the framework
*/
public TradfriColor(HSBType hsb) {
PercentType[] xyArray = hsb.toXY();
this.xyX = normalize(xyArray[0].doubleValue() / 100.0);
this.xyY = normalize(xyArray[1].doubleValue() / 100.0);
this.brightness = (int) (hsb.getBrightness().floatValue() * 2.54);
}
/**
* Obtain the TradfriColor (x/y) as HSBType
*
* @return HSBType representing the x/y Tradfri color
*/
public HSBType getHSB() {
float x = unnormalize(xyX);
float y = unnormalize(xyY);
HSBType converted = HSBType.fromXY(x, y);
final Integer brightness = this.brightness;
if (brightness == null) {
throw new IllegalStateException("cannot convert to HSB with brightness=null");
}
return new HSBType(converted.getHue(), converted.getSaturation(), xyBrightnessToPercentType(brightness));
}
/**
* Construct from color temperature in percent.
* 0 (coldest) to 100 (warmest).
* Note: The resulting {@link TradfriColor} has only the {@link TradfriColor#xyX X} and {@link TradfriColor#xyY y}
* values set!
*
* @param percentType the color temperature in percent
*/
public TradfriColor(PercentType percentType) {
double percent = percentType.doubleValue();
int x, y;
if (percent < 50.0) {
// we calculate a value that is between preset 0 and 1
double p = percent / 50.0;
x = (int) Math.round(PRESET_X[0] + p * (PRESET_X[1] - PRESET_X[0]));
y = (int) Math.round(PRESET_Y[0] + p * (PRESET_Y[1] - PRESET_Y[0]));
} else {
// we calculate a value that is between preset 1 and 2
double p = (percent - 50) / 50.0;
x = (int) Math.round(PRESET_X[1] + p * (PRESET_X[2] - PRESET_X[1]));
y = (int) Math.round(PRESET_Y[1] + p * (PRESET_Y[2] - PRESET_Y[1]));
}
this.xyX = x;
this.xyY = y;
}
/**
* Normalize value to the tradfri range.
*
* @param value double in the range 0.0 to 1.0
* @return normalized value in the range 0 to 65535
*/
private int normalize(double value) {
return (int) (value * 65535 + 0.5);
}
/**
* Reverse-normalize value from the tradfri range.
*
* @param value integer in the range 0 to 65535
* @return unnormalized value in the range 0.0 to 1.0
*/
private float unnormalize(int value) {
return (value / 65535.0f);
}
/**
* Calculate the color temperature from given x and y values.
*
* @return {@link PercentType} with color temperature (0 = coolest, 100 = warmest)
*/
public PercentType getColorTemperature() {
double x = xyX;
double y = xyY;
double value = 0.0;
if ((x > PRESET_X[1] && y > PRESET_Y[1]) && (x <= PRESET_X[2] && y <= PRESET_Y[2])) {
// is it between preset 1 and 2?
value = (x - PRESET_X[1]) / (PRESET_X[2] - PRESET_X[1]) / 2.0 + 0.5;
} else if ((x >= PRESET_X[0] && y >= PRESET_Y[0]) && (x <= (PRESET_X[1] + 2.0) && y <= PRESET_Y[1])) {
// is it between preset 0 and 1?
// hint: in the above line we calculate 2.0 to PRESET_X[1] because
// some bulbs send slighty higher x values for this preset (maybe rounding errors?)
value = (x - PRESET_X[0]) / (PRESET_X[1] - PRESET_X[0]) / 2.0;
} else if (x < PRESET_X[0]) {
// cooler than coolest preset (full color bulbs)
value = 0.0;
} else if (x > PRESET_X[2]) {
// warmer than warmest preset (full color bulbs)
value = 1.0;
}
return new PercentType((int) Math.round(value * 100.0));
}
/**
* Converts the xyBrightness value to PercentType
*
* @param xyBrightness xy brightness level 0 to 254
* @return {@link PercentType} with brightness level (0 = light is off, 1 = lowest, 100 = highest)
*/
public static PercentType xyBrightnessToPercentType(int xyBrightness) {
if (xyBrightness > 254) {
return PercentType.HUNDRED;
} else if (xyBrightness < 0) {
return PercentType.ZERO;
}
return new PercentType((int) Math.ceil(xyBrightness / 2.54));
}
}

View File

@@ -0,0 +1,69 @@
/**
* 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.tradfri.internal;
import static org.openhab.binding.tradfri.internal.TradfriBindingConstants.*;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.tradfri.internal.handler.TradfriBlindHandler;
import org.openhab.binding.tradfri.internal.handler.TradfriControllerHandler;
import org.openhab.binding.tradfri.internal.handler.TradfriGatewayHandler;
import org.openhab.binding.tradfri.internal.handler.TradfriLightHandler;
import org.openhab.binding.tradfri.internal.handler.TradfriPlugHandler;
import org.openhab.binding.tradfri.internal.handler.TradfriSensorHandler;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerFactory;
import org.osgi.service.component.annotations.Component;
/**
* The {@link TradfriHandlerFactory} is responsible for creating things and thing handlers.
*
* @author Kai Kreuzer - Initial contribution
* @author Christoph Weitkamp - Added support for remote controller and motion sensor devices (read-only battery level)
* @author Manuel Raffel - Added support for blinds
*/
@Component(service = ThingHandlerFactory.class, configurationPid = "binding.tradfri")
@NonNullByDefault
public class TradfriHandlerFactory extends BaseThingHandlerFactory {
@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 (GATEWAY_TYPE_UID.equals(thingTypeUID)) {
return new TradfriGatewayHandler((Bridge) thing);
} else if (THING_TYPE_DIMMER.equals(thingTypeUID) || THING_TYPE_REMOTE_CONTROL.equals(thingTypeUID)
|| THING_TYPE_OPEN_CLOSE_REMOTE_CONTROL.equals(thingTypeUID)) {
return new TradfriControllerHandler(thing);
} else if (THING_TYPE_MOTION_SENSOR.equals(thingTypeUID)) {
return new TradfriSensorHandler(thing);
} else if (THING_TYPE_BLINDS.equals(thingTypeUID)) {
return new TradfriBlindHandler(thing);
} else if (SUPPORTED_LIGHT_TYPES_UIDS.contains(thingTypeUID)) {
return new TradfriLightHandler(thing);
} else if (SUPPORTED_PLUG_TYPES_UIDS.contains(thingTypeUID)) {
return new TradfriPlugHandler(thing);
}
return null;
}
}

View File

@@ -0,0 +1,26 @@
/**
* 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.tradfri.internal.config;
/**
* The {@link TradfriDeviceConfig} holds the
* configuration information needed to access single bulbs on the gateway.
*
* @author Kai Kreuzer - Initial contribution
*/
public class TradfriDeviceConfig {
public static final String CONFIG_ID = "id";
public Integer id;
}

View File

@@ -0,0 +1,27 @@
/**
* 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.tradfri.internal.config;
/**
* Configuration class for the gateway.
*
* @author Kai Kreuzer - Initial contribution
*/
public class TradfriGatewayConfig {
public String host;
public int port = 5684; // default port
public String code;
public String identity;
public String preSharedKey;
}

View File

@@ -0,0 +1,103 @@
/**
* 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.tradfri.internal.discovery;
import static org.openhab.binding.tradfri.internal.TradfriBindingConstants.*;
import static org.openhab.core.thing.Thing.*;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.jmdns.ServiceInfo;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
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.ThingTypeUID;
import org.openhab.core.thing.ThingUID;
import org.osgi.service.component.annotations.Component;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This class identifies Tradfri gateways by their mDNS service information.
*
* @author Kai Kreuzer - Initial contribution
*/
@Component(service = MDNSDiscoveryParticipant.class, immediate = true)
@NonNullByDefault
public class TradfriDiscoveryParticipant implements MDNSDiscoveryParticipant {
private final Logger logger = LoggerFactory.getLogger(TradfriDiscoveryParticipant.class);
private static final String SERVICE_TYPE = "_coap._udp.local.";
/**
* RegEx patter to match the gateway name announced by mDNS
* Possible values:
* gw:001122334455, gw-001122334455, gw:00-11-22-33-44-55, gw-001122334455ServiceName
*
*/
private static final Pattern GATEWAY_NAME_REGEX_PATTERN = Pattern.compile("(gw[:-]{1}([a-f0-9]{2}[-]?){6}){1}");
@Override
public Set<ThingTypeUID> getSupportedThingTypeUIDs() {
return SUPPORTED_BRIDGE_TYPES_UIDS;
}
@Override
public String getServiceType() {
return SERVICE_TYPE;
}
@Override
public @Nullable ThingUID getThingUID(@Nullable ServiceInfo service) {
if (service != null) {
Matcher m = GATEWAY_NAME_REGEX_PATTERN.matcher(service.getName());
if (m.find()) {
return new ThingUID(GATEWAY_TYPE_UID, m.group(1).replaceAll("[^A-Za-z0-9_]", ""));
}
}
return null;
}
@Override
public @Nullable DiscoveryResult createResult(ServiceInfo service) {
ThingUID thingUID = getThingUID(service);
if (thingUID != null) {
if (service.getHostAddresses() != null && service.getHostAddresses().length > 0
&& !service.getHostAddresses()[0].isEmpty()) {
logger.debug("Discovered Tradfri gateway: {}", service);
Map<String, Object> properties = new HashMap<>(4);
properties.put(PROPERTY_VENDOR, "IKEA of Sweden");
properties.put(GATEWAY_CONFIG_HOST, service.getHostAddresses()[0]);
properties.put(GATEWAY_CONFIG_PORT, service.getPort());
properties.put(PROPERTY_SERIAL_NUMBER, service.getName());
String fwVersion = service.getPropertyString("version");
if (fwVersion != null) {
properties.put(PROPERTY_FIRMWARE_VERSION, fwVersion);
}
return DiscoveryResultBuilder.create(thingUID).withProperties(properties).withLabel("TRÅDFRI Gateway")
.withRepresentationProperty(GATEWAY_CONFIG_HOST).build();
} else {
logger.warn("Discovered Tradfri gateway doesn't have an IP address: {}", service);
}
}
return null;
}
}

View File

@@ -0,0 +1,193 @@
/**
* 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.tradfri.internal.discovery;
import static org.openhab.binding.tradfri.internal.TradfriBindingConstants.*;
import static org.openhab.core.thing.Thing.*;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
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.openhab.binding.tradfri.internal.DeviceUpdateListener;
import org.openhab.binding.tradfri.internal.handler.TradfriGatewayHandler;
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.config.discovery.DiscoveryService;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.JsonObject;
import com.google.gson.JsonSyntaxException;
/**
* This class identifies devices that are available on the gateway and adds discovery results for them.
*
* @author Kai Kreuzer - Initial contribution
* @author Christoph Weitkamp - Added support for remote controller and motion sensor devices (read-only battery level)
* @author Andre Fuechsel - fixed the results removal
* @author Manuel Raffel - Added support for blinds
*/
@NonNullByDefault
public class TradfriDiscoveryService extends AbstractDiscoveryService
implements DeviceUpdateListener, DiscoveryService, ThingHandlerService {
private final Logger logger = LoggerFactory.getLogger(TradfriDiscoveryService.class);
private @Nullable TradfriGatewayHandler handler;
private static final String REMOTE_CONTROLLER_MODEL = "TRADFRI remote control";
private static final Set<String> COLOR_TEMP_MODELS = Collections
.unmodifiableSet(Stream
.of("TRADFRI bulb E27 WS opal 980lm", "TRADFRI bulb E27 WS clear 950lm",
"TRADFRI bulb GU10 WS 400lm", "TRADFRI bulb E14 WS opal 400lm", "FLOALT panel WS 30x30",
"FLOALT panel WS 60x60", "FLOALT panel WS 30x90", "TRADFRI bulb E12 WS opal 400lm")
.collect(Collectors.toSet()));
private static final String[] COLOR_MODEL_IDENTIFIER_HINTS = new String[] { "CWS", " C/WS " };
public TradfriDiscoveryService() {
super(SUPPORTED_DEVICE_TYPES_UIDS, 10, true);
}
@Override
protected void startScan() {
handler.startScan();
}
@Override
protected synchronized void stopScan() {
super.stopScan();
removeOlderResults(getTimestampOfLastScan());
}
@Override
public void setThingHandler(@Nullable ThingHandler handler) {
if (handler instanceof TradfriGatewayHandler) {
this.handler = (TradfriGatewayHandler) handler;
}
}
@Override
public @Nullable ThingHandler getThingHandler() {
return handler;
}
@Override
public void activate() {
handler.registerDeviceUpdateListener(this);
}
@Override
public void deactivate() {
removeOlderResults(new Date().getTime());
handler.unregisterDeviceUpdateListener(this);
}
@Override
public void onUpdate(@Nullable String instanceId, @Nullable JsonObject data) {
ThingUID bridge = handler.getThing().getUID();
try {
if (data != null && data.has(INSTANCE_ID)) {
int id = data.get(INSTANCE_ID).getAsInt();
String type = data.get(TYPE).getAsString();
JsonObject deviceInfo = data.get(DEVICE).getAsJsonObject();
String model = deviceInfo.get(DEVICE_MODEL).getAsString();
ThingUID thingId = null;
if (TYPE_LIGHT.equals(type) && data.has(LIGHT)) {
JsonObject state = data.get(LIGHT).getAsJsonArray().get(0).getAsJsonObject();
// Color temperature light:
// We do not always receive a COLOR attribute, even the light supports it - but the gateway does not
// seem to have this information, if the bulb is unreachable. We therefore also check against
// concrete model names.
// Color light:
// As the protocol does not distinguishes between color and full-color lights,
// we check if the "CWS" or "CW/S" identifier is given in the model name
ThingTypeUID thingType = null;
if (model != null && Arrays.stream(COLOR_MODEL_IDENTIFIER_HINTS).anyMatch(model::contains)) {
thingType = THING_TYPE_COLOR_LIGHT;
}
if (thingType == null && //
(state.has(COLOR) || (model != null && COLOR_TEMP_MODELS.contains(model)))) {
thingType = THING_TYPE_COLOR_TEMP_LIGHT;
}
if (thingType == null) {
thingType = THING_TYPE_DIMMABLE_LIGHT;
}
thingId = new ThingUID(thingType, bridge, Integer.toString(id));
} else if (TYPE_BLINDS.equals(type) && data.has(BLINDS)) {
// Blinds
thingId = new ThingUID(THING_TYPE_BLINDS, bridge, Integer.toString(id));
} else if (TYPE_PLUG.equals(type) && data.has(PLUG)) {
// Smart plug
thingId = new ThingUID(THING_TYPE_ONOFF_PLUG, bridge, Integer.toString(id));
} else if (TYPE_SWITCH.equals(type) && data.has(SWITCH)) {
// Remote control and wireless dimmer
// As protocol does not distinguishes between remote control and wireless dimmer,
// we check for the whole model name
ThingTypeUID thingType = (model != null && REMOTE_CONTROLLER_MODEL.equals(model))
? THING_TYPE_REMOTE_CONTROL
: THING_TYPE_DIMMER;
thingId = new ThingUID(thingType, bridge, Integer.toString(id));
} else if (TYPE_REMOTE.equals(type)) {
thingId = new ThingUID(THING_TYPE_OPEN_CLOSE_REMOTE_CONTROL, bridge, Integer.toString(id));
} else if (TYPE_SENSOR.equals(type) && data.has(SENSOR)) {
// Motion sensor
thingId = new ThingUID(THING_TYPE_MOTION_SENSOR, bridge, Integer.toString(id));
}
if (thingId == null) {
// we didn't identify any device, so let's quit
logger.debug("Ignoring unknown device on TRADFRI gateway:\n\ttype : {}\n\tmodel: {}\n\tinfo : {}",
type, model, deviceInfo.getAsString());
return;
}
String label = data.get(NAME).getAsString();
Map<String, Object> properties = new HashMap<>(1);
properties.put("id", id);
if (model != null) {
properties.put(PROPERTY_MODEL_ID, model);
}
if (deviceInfo.get(DEVICE_VENDOR) != null) {
properties.put(PROPERTY_VENDOR, deviceInfo.get(DEVICE_VENDOR).getAsString());
}
if (deviceInfo.get(DEVICE_FIRMWARE) != null) {
properties.put(PROPERTY_FIRMWARE_VERSION, deviceInfo.get(DEVICE_FIRMWARE).getAsString());
}
logger.debug("Adding device {} to inbox", thingId);
DiscoveryResult discoveryResult = DiscoveryResultBuilder.create(thingId).withBridge(bridge)
.withLabel(label).withProperties(properties).withRepresentationProperty("id").build();
thingDiscovered(discoveryResult);
}
} catch (JsonSyntaxException e) {
logger.debug("JSON error during discovery: {}", e.getMessage());
}
}
}

View File

@@ -0,0 +1,123 @@
/**
* 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.tradfri.internal.handler;
import static org.openhab.binding.tradfri.internal.TradfriBindingConstants.*;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.tradfri.internal.model.TradfriBlindData;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.PercentType;
import org.openhab.core.library.types.StopMoveType;
import org.openhab.core.library.types.UpDownType;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.JsonElement;
/**
* The {@link TradfriBlindHandler} is responsible for handling commands for individual blinds.
*
* @author Manuel Raffel - Initial contribution
*/
@NonNullByDefault
public class TradfriBlindHandler extends TradfriThingHandler {
private final Logger logger = LoggerFactory.getLogger(TradfriBlindHandler.class);
public TradfriBlindHandler(Thing thing) {
super(thing);
}
@Override
public void onUpdate(JsonElement data) {
if (active && !(data.isJsonNull())) {
TradfriBlindData state = new TradfriBlindData(data);
updateStatus(state.getReachabilityStatus() ? ThingStatus.ONLINE : ThingStatus.OFFLINE);
PercentType position = state.getPosition();
if (position != null) {
updateState(CHANNEL_POSITION, position);
}
DecimalType batteryLevel = state.getBatteryLevel();
if (batteryLevel != null) {
updateState(CHANNEL_BATTERY_LEVEL, batteryLevel);
}
OnOffType batteryLow = state.getBatteryLow();
if (batteryLow != null) {
updateState(CHANNEL_BATTERY_LOW, batteryLow);
}
updateDeviceProperties(state);
logger.debug(
"Updating thing for blindId {} to state {position: {}, firmwareVersion: {}, modelId: {}, vendor: {}}",
state.getDeviceId(), position, state.getFirmwareVersion(), state.getModelId(), state.getVendor());
}
}
private void setPosition(PercentType percent) {
set(new TradfriBlindData().setPosition(percent).getJsonString());
}
private void triggerStop() {
set(new TradfriBlindData().stop().getJsonString());
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
if (active) {
if (command instanceof RefreshType) {
logger.debug("Refreshing channel {}", channelUID);
coapClient.asyncGet(this);
return;
}
switch (channelUID.getId()) {
case CHANNEL_POSITION:
handlePositionCommand(command);
break;
default:
logger.error("Unknown channel UID {}", channelUID);
}
}
}
private void handlePositionCommand(Command command) {
if (command instanceof PercentType) {
setPosition((PercentType) command);
} else if (command instanceof StopMoveType) {
if (StopMoveType.STOP.equals(command)) {
triggerStop();
} else {
logger.debug("Cannot handle command '{}' for channel '{}'", command, CHANNEL_POSITION);
}
} else if (command instanceof UpDownType) {
if (UpDownType.UP.equals(command)) {
setPosition(PercentType.ZERO);
} else {
setPosition(PercentType.HUNDRED);
}
} else {
logger.debug("Cannot handle command '{}' for channel '{}'", command, CHANNEL_POSITION);
}
}
}

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.tradfri.internal.handler;
import static org.openhab.binding.tradfri.internal.TradfriBindingConstants.*;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.tradfri.internal.model.TradfriControllerData;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.JsonElement;
/**
* The {@link TradfriControllerHandler} is responsible for handling commands for individual controllers.
*
* @author Christoph Weitkamp - Initial contribution
*/
@NonNullByDefault
public class TradfriControllerHandler extends TradfriThingHandler {
private final Logger logger = LoggerFactory.getLogger(TradfriControllerHandler.class);
// keeps track of the current state for handling of increase/decrease
private @Nullable TradfriControllerData state;
public TradfriControllerHandler(Thing thing) {
super(thing);
}
@Override
public void onUpdate(JsonElement data) {
if (active && !(data.isJsonNull())) {
state = new TradfriControllerData(data);
updateStatus(state.getReachabilityStatus() ? ThingStatus.ONLINE : ThingStatus.OFFLINE);
final TradfriControllerData state = this.state;
if (state == null) {
return;
}
DecimalType batteryLevel = state.getBatteryLevel();
if (batteryLevel != null) {
updateState(CHANNEL_BATTERY_LEVEL, batteryLevel);
}
OnOffType batteryLow = state.getBatteryLow();
if (batteryLow != null) {
updateState(CHANNEL_BATTERY_LOW, batteryLow);
}
updateDeviceProperties(state);
logger.debug(
"Updating thing for controllerId {} to state {batteryLevel: {}, batteryLow: {}, firmwareVersion: {}, modelId: {}, vendor: {}}",
state.getDeviceId(), batteryLevel, batteryLow, state.getFirmwareVersion(), state.getModelId(),
state.getVendor());
}
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
if (active) {
if (command instanceof RefreshType) {
logger.debug("Refreshing channel {}", channelUID);
coapClient.asyncGet(this);
return;
}
logger.debug("The controller is a read-only device and cannot handle commands.");
}
}
}

View File

@@ -0,0 +1,399 @@
/**
* 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.tradfri.internal.handler;
import static org.openhab.binding.tradfri.internal.TradfriBindingConstants.*;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Collection;
import java.util.Collections;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import org.eclipse.californium.core.CoapClient;
import org.eclipse.californium.core.CoapResponse;
import org.eclipse.californium.core.network.CoapEndpoint;
import org.eclipse.californium.elements.exception.ConnectorException;
import org.eclipse.californium.scandium.DTLSConnector;
import org.eclipse.californium.scandium.config.DtlsConnectorConfig;
import org.eclipse.californium.scandium.dtls.pskstore.StaticPskStore;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.tradfri.internal.CoapCallback;
import org.openhab.binding.tradfri.internal.DeviceUpdateListener;
import org.openhab.binding.tradfri.internal.TradfriBindingConstants;
import org.openhab.binding.tradfri.internal.TradfriCoapClient;
import org.openhab.binding.tradfri.internal.TradfriCoapHandler;
import org.openhab.binding.tradfri.internal.config.TradfriGatewayConfig;
import org.openhab.binding.tradfri.internal.discovery.TradfriDiscoveryService;
import org.openhab.binding.tradfri.internal.model.TradfriVersion;
import org.openhab.core.config.core.Configuration;
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.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerService;
import org.openhab.core.types.Command;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import com.google.gson.JsonParser;
import com.google.gson.JsonSyntaxException;
/**
* The {@link TradfriGatewayHandler} is responsible for handling commands, which are
* sent to one of the channels.
*
* @author Kai Kreuzer - Initial contribution
*/
@NonNullByDefault
public class TradfriGatewayHandler extends BaseBridgeHandler implements CoapCallback {
protected final Logger logger = LoggerFactory.getLogger(getClass());
private static final TradfriVersion MIN_SUPPORTED_VERSION = new TradfriVersion("1.2.42");
private @NonNullByDefault({}) TradfriCoapClient deviceClient;
private @NonNullByDefault({}) String gatewayURI;
private @NonNullByDefault({}) String gatewayInfoURI;
private @NonNullByDefault({}) DTLSConnector dtlsConnector;
private @Nullable CoapEndpoint endPoint;
private final Set<DeviceUpdateListener> deviceUpdateListeners = new CopyOnWriteArraySet<>();
private @Nullable ScheduledFuture<?> scanJob;
public TradfriGatewayHandler(Bridge bridge) {
super(bridge);
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
// there are no channels on the gateway yet
}
@Override
public void initialize() {
TradfriGatewayConfig configuration = getConfigAs(TradfriGatewayConfig.class);
if (isNullOrEmpty(configuration.host)) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"Host must be specified in the configuration!");
return;
}
if (isNullOrEmpty(configuration.code)) {
if (isNullOrEmpty(configuration.identity) || isNullOrEmpty(configuration.preSharedKey)) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"Either security code or identity and pre-shared key must be provided in the configuration!");
return;
} else {
establishConnection();
}
} else {
String currentFirmware = thing.getProperties().get(Thing.PROPERTY_FIRMWARE_VERSION);
if (!isNullOrEmpty(currentFirmware)
&& MIN_SUPPORTED_VERSION.compareTo(new TradfriVersion(currentFirmware)) > 0) {
// older firmware not supported
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
String.format(
"Gateway firmware version '%s' is too old! Minimum supported firmware version is '%s'.",
currentFirmware, MIN_SUPPORTED_VERSION.toString()));
return;
}
// Running async operation to retrieve new <'identity','key'> pair
scheduler.execute(() -> {
boolean success = obtainIdentityAndPreSharedKey();
if (success) {
establishConnection();
}
});
}
}
@Override
public Collection<Class<? extends ThingHandlerService>> getServices() {
return Collections.singleton(TradfriDiscoveryService.class);
}
private void establishConnection() {
TradfriGatewayConfig configuration = getConfigAs(TradfriGatewayConfig.class);
this.gatewayURI = "coaps://" + configuration.host + ":" + configuration.port + "/" + DEVICES;
this.gatewayInfoURI = "coaps://" + configuration.host + ":" + configuration.port + "/" + GATEWAY + "/"
+ GATEWAY_DETAILS;
try {
URI uri = new URI(gatewayURI);
deviceClient = new TradfriCoapClient(uri);
} catch (URISyntaxException e) {
logger.error("Illegal gateway URI '{}': {}", gatewayURI, e.getMessage());
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
return;
}
DtlsConnectorConfig.Builder builder = new DtlsConnectorConfig.Builder();
builder.setPskStore(new StaticPskStore(configuration.identity, configuration.preSharedKey.getBytes()));
builder.setMaxConnections(100);
builder.setStaleConnectionThreshold(60);
dtlsConnector = new DTLSConnector(builder.build());
endPoint = new CoapEndpoint.Builder().setConnector(dtlsConnector).build();
deviceClient.setEndpoint(endPoint);
updateStatus(ThingStatus.UNKNOWN);
// schedule a new scan every minute
scanJob = scheduler.scheduleWithFixedDelay(this::startScan, 0, 1, TimeUnit.MINUTES);
}
/**
* Authenticates against the gateway with the security code in order to receive a pre-shared key for a newly
* generated identity.
* As this requires a remote request, this method might be long-running.
*
* @return true, if credentials were successfully obtained, false otherwise
*/
protected boolean obtainIdentityAndPreSharedKey() {
TradfriGatewayConfig configuration = getConfigAs(TradfriGatewayConfig.class);
String identity = UUID.randomUUID().toString().replace("-", "");
String preSharedKey = null;
CoapResponse gatewayResponse;
String authUrl = null;
String responseText = null;
try {
DtlsConnectorConfig.Builder builder = new DtlsConnectorConfig.Builder();
builder.setPskStore(new StaticPskStore("Client_identity", configuration.code.getBytes()));
DTLSConnector dtlsConnector = new DTLSConnector(builder.build());
CoapEndpoint.Builder authEndpointBuilder = new CoapEndpoint.Builder();
authEndpointBuilder.setConnector(dtlsConnector);
CoapEndpoint authEndpoint = authEndpointBuilder.build();
authUrl = "coaps://" + configuration.host + ":" + configuration.port + "/15011/9063";
CoapClient deviceClient = new CoapClient(new URI(authUrl));
deviceClient.setTimeout(TimeUnit.SECONDS.toMillis(10));
deviceClient.setEndpoint(authEndpoint);
JsonObject json = new JsonObject();
json.addProperty(CLIENT_IDENTITY_PROPOSED, identity);
gatewayResponse = deviceClient.post(json.toString(), 0);
authEndpoint.destroy();
deviceClient.shutdown();
if (gatewayResponse == null) {
// seems we ran in a timeout, which potentially also happens
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"No response from gateway. Might be due to an invalid security code.");
return false;
}
if (gatewayResponse.isSuccess()) {
responseText = gatewayResponse.getResponseText();
json = new JsonParser().parse(responseText).getAsJsonObject();
preSharedKey = json.get(NEW_PSK_BY_GW).getAsString();
if (isNullOrEmpty(preSharedKey)) {
logger.error("Received pre-shared key is empty for thing {} on gateway at {}", getThing().getUID(),
configuration.host);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"Pre-shared key was not obtain successfully");
return false;
} else {
logger.info("Received pre-shared key for gateway '{}'", configuration.host);
logger.debug("Using identity '{}' with pre-shared key '{}'.", identity, preSharedKey);
Configuration editedConfig = editConfiguration();
editedConfig.put(TradfriBindingConstants.GATEWAY_CONFIG_CODE, null);
editedConfig.put(TradfriBindingConstants.GATEWAY_CONFIG_IDENTITY, identity);
editedConfig.put(TradfriBindingConstants.GATEWAY_CONFIG_PRE_SHARED_KEY, preSharedKey);
updateConfiguration(editedConfig);
return true;
}
} else {
logger.warn(
"Failed obtaining pre-shared key for identity '{}' (response code '{}', response text '{}')",
identity, gatewayResponse.getCode(),
isNullOrEmpty(gatewayResponse.getResponseText()) ? "<empty>"
: gatewayResponse.getResponseText());
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, String
.format("Failed obtaining pre-shared key with status code '%s'", gatewayResponse.getCode()));
}
} catch (URISyntaxException e) {
logger.error("Illegal gateway URI '{}'", authUrl, e);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
} catch (JsonParseException e) {
logger.warn("Invalid response received from gateway '{}'", responseText, e);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
String.format("Invalid response received from gateway '%s'", responseText));
} catch (ConnectorException | IOException e) {
logger.debug("Error connecting to gateway ", e);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
String.format("Error connecting to gateway."));
}
return false;
}
@Override
public void dispose() {
if (scanJob != null) {
scanJob.cancel(true);
scanJob = null;
}
if (endPoint != null) {
endPoint.destroy();
endPoint = null;
}
if (deviceClient != null) {
deviceClient.shutdown();
deviceClient = null;
}
super.dispose();
}
/**
* Does a request to the gateway to list all available devices/services.
* The response is received and processed by the method {@link onUpdate(JsonElement data)}.
*/
public void startScan() {
if (endPoint != null) {
requestGatewayInfo();
deviceClient.get(new TradfriCoapHandler(this));
}
}
/**
* Returns the root URI of the gateway.
*
* @return root URI of the gateway with coaps scheme
*/
public String getGatewayURI() {
return gatewayURI;
}
/**
* Returns the coap endpoint that can be used within coap clients.
*
* @return the coap endpoint
*/
public @Nullable CoapEndpoint getEndpoint() {
return endPoint;
}
@Override
public void onUpdate(JsonElement data) {
logger.debug("onUpdate response: {}", data);
if (endPoint != null) {
try {
JsonArray array = data.getAsJsonArray();
for (int i = 0; i < array.size(); i++) {
requestDeviceDetails(array.get(i).getAsString());
}
} catch (JsonSyntaxException e) {
logger.debug("JSON error: {}", e.getMessage());
setStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
}
}
}
private synchronized void requestGatewayInfo() {
// we are reusing our coap client and merely temporarily set a gateway info to call
deviceClient.setURI(gatewayInfoURI);
deviceClient.asyncGet().thenAccept(data -> {
logger.debug("requestGatewayInfo response: {}", data);
JsonObject json = new JsonParser().parse(data).getAsJsonObject();
String firmwareVersion = json.get(VERSION).getAsString();
getThing().setProperty(Thing.PROPERTY_FIRMWARE_VERSION, firmwareVersion);
updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE);
});
// restore root URI
deviceClient.setURI(gatewayURI);
}
private synchronized void requestDeviceDetails(String instanceId) {
// we are reusing our coap client and merely temporarily set a sub-URI to call
deviceClient.setURI(gatewayURI + "/" + instanceId);
deviceClient.asyncGet().thenAccept(data -> {
logger.debug("requestDeviceDetails response: {}", data);
JsonObject json = new JsonParser().parse(data).getAsJsonObject();
deviceUpdateListeners.forEach(listener -> listener.onUpdate(instanceId, json));
});
// restore root URI
deviceClient.setURI(gatewayURI);
}
@Override
public void setStatus(ThingStatus status, ThingStatusDetail statusDetail) {
// to fix connection issues after a gateway reboot, a session resume is forced for the next command
if (status == ThingStatus.OFFLINE && statusDetail == ThingStatusDetail.COMMUNICATION_ERROR) {
logger.debug("Gateway communication error. Forcing a re-initialization!");
dispose();
initialize();
}
// are we still connected at all?
if (endPoint != null) {
updateStatus(status, statusDetail);
}
}
/**
* Registers a listener, which is informed about device details.
*
* @param listener the listener to register
*/
public void registerDeviceUpdateListener(DeviceUpdateListener listener) {
this.deviceUpdateListeners.add(listener);
}
/**
* Unregisters a given listener.
*
* @param listener the listener to unregister
*/
public void unregisterDeviceUpdateListener(DeviceUpdateListener listener) {
this.deviceUpdateListeners.remove(listener);
}
private boolean isNullOrEmpty(@Nullable String string) {
return string == null || string.isEmpty();
}
@Override
public void thingUpdated(Thing thing) {
super.thingUpdated(thing);
logger.info("Bridge configuration updated. Updating paired things (if any).");
for (Thing t : getThing().getThings()) {
final ThingHandler thingHandler = t.getHandler();
if (thingHandler != null) {
thingHandler.thingUpdated(t);
}
}
}
}

View File

@@ -0,0 +1,230 @@
/**
* 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.tradfri.internal.handler;
import static org.openhab.binding.tradfri.internal.TradfriBindingConstants.*;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.tradfri.internal.model.TradfriLightData;
import org.openhab.core.library.types.HSBType;
import org.openhab.core.library.types.IncreaseDecreaseType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.PercentType;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.JsonElement;
/**
* The {@link TradfriLightHandler} is responsible for handling commands for individual lights.
*
* @author Kai Kreuzer - Initial contribution
* @author Holger Reichert - Support for color bulbs
* @author Christoph Weitkamp - Restructuring and refactoring of the binding
*/
@NonNullByDefault
public class TradfriLightHandler extends TradfriThingHandler {
private final Logger logger = LoggerFactory.getLogger(TradfriLightHandler.class);
// step size for increase/decrease commands
private static final int STEP = 10;
// keeps track of the current state for handling of increase/decrease
private @Nullable TradfriLightData state;
public TradfriLightHandler(Thing thing) {
super(thing);
}
@Override
public void onUpdate(JsonElement data) {
if (active && !(data.isJsonNull())) {
TradfriLightData state = new TradfriLightData(data);
updateStatus(state.getReachabilityStatus() ? ThingStatus.ONLINE : ThingStatus.OFFLINE);
if (!state.getOnOffState()) {
logger.debug("Setting state to OFF");
updateState(CHANNEL_BRIGHTNESS, PercentType.ZERO);
if (lightHasColorSupport()) {
updateState(CHANNEL_COLOR, HSBType.BLACK);
}
// if we are turned off, we do not set any brightness value
return;
}
PercentType dimmer = state.getBrightness();
if (dimmer != null && !lightHasColorSupport()) { // color lights do not have brightness channel
updateState(CHANNEL_BRIGHTNESS, dimmer);
}
PercentType colorTemp = state.getColorTemperature();
if (colorTemp != null) {
updateState(CHANNEL_COLOR_TEMPERATURE, colorTemp);
}
HSBType color = null;
if (lightHasColorSupport()) {
color = state.getColor();
if (color != null) {
updateState(CHANNEL_COLOR, color);
}
}
updateDeviceProperties(state);
this.state = state;
logger.debug(
"Updating thing for lightId {} to state {dimmer: {}, colorTemp: {}, color: {}, firmwareVersion: {}, modelId: {}, vendor: {}}",
state.getDeviceId(), dimmer, colorTemp, color, state.getFirmwareVersion(), state.getModelId(),
state.getVendor());
}
}
private void setBrightness(PercentType percent) {
TradfriLightData data = new TradfriLightData();
data.setBrightness(percent).setTransitionTime(DEFAULT_DIMMER_TRANSITION_TIME);
set(data.getJsonString());
}
private void setState(OnOffType onOff) {
TradfriLightData data = new TradfriLightData();
data.setOnOffState(onOff == OnOffType.ON);
set(data.getJsonString());
}
private void setColorTemperature(PercentType percent) {
TradfriLightData data = new TradfriLightData();
data.setColorTemperature(percent).setTransitionTime(DEFAULT_DIMMER_TRANSITION_TIME);
set(data.getJsonString());
}
private void setColor(HSBType hsb) {
TradfriLightData data = new TradfriLightData();
data.setColor(hsb).setTransitionTime(DEFAULT_DIMMER_TRANSITION_TIME);
set(data.getJsonString());
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
if (active) {
if (command instanceof RefreshType) {
logger.debug("Refreshing channel {}", channelUID);
coapClient.asyncGet(this);
return;
}
switch (channelUID.getId()) {
case CHANNEL_BRIGHTNESS:
handleBrightnessCommand(command);
break;
case CHANNEL_COLOR_TEMPERATURE:
handleColorTemperatureCommand(command);
break;
case CHANNEL_COLOR:
handleColorCommand(command);
break;
default:
logger.error("Unknown channel UID {}", channelUID);
}
}
}
private void handleBrightnessCommand(Command command) {
if (command instanceof PercentType) {
setBrightness((PercentType) command);
} else if (command instanceof OnOffType) {
setState(((OnOffType) command));
} else if (command instanceof IncreaseDecreaseType) {
final TradfriLightData state = this.state;
if (state != null && state.getBrightness() != null) {
@SuppressWarnings("null")
int current = state.getBrightness().intValue();
if (IncreaseDecreaseType.INCREASE.equals(command)) {
setBrightness(new PercentType(Math.min(current + STEP, PercentType.HUNDRED.intValue())));
} else {
setBrightness(new PercentType(Math.max(current - STEP, PercentType.ZERO.intValue())));
}
} else {
logger.debug("Cannot handle inc/dec as current state is not known.");
}
} else {
logger.debug("Cannot handle command {} for channel {}", command, CHANNEL_BRIGHTNESS);
}
}
private void handleColorTemperatureCommand(Command command) {
if (command instanceof PercentType) {
setColorTemperature((PercentType) command);
} else if (command instanceof IncreaseDecreaseType) {
final TradfriLightData state = this.state;
if (state != null && state.getColorTemperature() != null) {
@SuppressWarnings("null")
int current = state.getColorTemperature().intValue();
if (IncreaseDecreaseType.INCREASE.equals(command)) {
setColorTemperature(new PercentType(Math.min(current + STEP, PercentType.HUNDRED.intValue())));
} else {
setColorTemperature(new PercentType(Math.max(current - STEP, PercentType.ZERO.intValue())));
}
} else {
logger.debug("Cannot handle inc/dec as current state is not known.");
}
} else {
logger.debug("Can't handle command {} on channel {}", command, CHANNEL_COLOR_TEMPERATURE);
}
}
private void handleColorCommand(Command command) {
if (command instanceof HSBType) {
setColor((HSBType) command);
setBrightness(((HSBType) command).getBrightness());
} else if (command instanceof OnOffType) {
setState(((OnOffType) command));
} else if (command instanceof PercentType) {
// PaperUI sends PercentType on color channel when changing Brightness
setBrightness((PercentType) command);
} else if (command instanceof IncreaseDecreaseType) {
final TradfriLightData state = this.state;
// increase or decrease only the brightness, but keep color
if (state != null && state.getBrightness() != null) {
@SuppressWarnings("null")
int current = state.getBrightness().intValue();
if (IncreaseDecreaseType.INCREASE.equals(command)) {
setBrightness(new PercentType(Math.min(current + STEP, PercentType.HUNDRED.intValue())));
} else {
setBrightness(new PercentType(Math.max(current - STEP, PercentType.ZERO.intValue())));
}
} else {
logger.debug("Cannot handle inc/dec for color as current brightness is not known.");
}
} else {
logger.debug("Can't handle command {} on channel {}", command, CHANNEL_COLOR);
}
}
/**
* Checks if this light supports full color.
*
* @return true if the light supports full color
*/
private boolean lightHasColorSupport() {
return thing.getThingTypeUID().getId().equals(THING_TYPE_COLOR_LIGHT.getId());
}
}

View File

@@ -0,0 +1,83 @@
/**
* 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.tradfri.internal.handler;
import static org.openhab.binding.tradfri.internal.TradfriBindingConstants.CHANNEL_POWER;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.tradfri.internal.model.TradfriPlugData;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.JsonElement;
/**
* The {@link TradfriPlugHandler} is responsible for handling commands for individual plugs.
*
* @author Kai Kreuzer - Initial contribution
*/
@NonNullByDefault
public class TradfriPlugHandler extends TradfriThingHandler {
private final Logger logger = LoggerFactory.getLogger(TradfriPlugHandler.class);
public TradfriPlugHandler(Thing thing) {
super(thing);
}
@Override
public void onUpdate(JsonElement data) {
if (active && !(data.isJsonNull())) {
TradfriPlugData state = new TradfriPlugData(data);
updateStatus(state.getReachabilityStatus() ? ThingStatus.ONLINE : ThingStatus.OFFLINE);
updateState(CHANNEL_POWER, state.getOnOffState() ? OnOffType.ON : OnOffType.OFF);
updateDeviceProperties(state);
}
}
private void setState(OnOffType onOff) {
TradfriPlugData data = new TradfriPlugData();
data.setOnOffState(onOff == OnOffType.ON);
set(data.getJsonString());
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
if (active) {
if (command instanceof RefreshType) {
logger.debug("Refreshing channel {}", channelUID);
coapClient.asyncGet(this);
return;
}
switch (channelUID.getId()) {
case CHANNEL_POWER:
if (command instanceof OnOffType) {
setState(((OnOffType) command));
} else {
logger.debug("Cannot handle command '{}' for channel '{}'", command, CHANNEL_POWER);
}
break;
default:
logger.error("Unknown channel UID {}", channelUID);
}
}
}
}

View File

@@ -0,0 +1,82 @@
/**
* 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.tradfri.internal.handler;
import static org.openhab.binding.tradfri.internal.TradfriBindingConstants.*;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.tradfri.internal.model.TradfriSensorData;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.JsonElement;
/**
* The {@link TradfriSensorHandler} is responsible for handling commands for individual sensors.
*
* @author Christoph Weitkamp - Initial contribution
*/
@NonNullByDefault
public class TradfriSensorHandler extends TradfriThingHandler {
private final Logger logger = LoggerFactory.getLogger(TradfriSensorHandler.class);
public TradfriSensorHandler(Thing thing) {
super(thing);
}
@Override
public void onUpdate(JsonElement data) {
if (active && !(data.isJsonNull())) {
TradfriSensorData state = new TradfriSensorData(data);
updateStatus(state.getReachabilityStatus() ? ThingStatus.ONLINE : ThingStatus.OFFLINE);
DecimalType batteryLevel = state.getBatteryLevel();
if (batteryLevel != null) {
updateState(CHANNEL_BATTERY_LEVEL, batteryLevel);
}
OnOffType batteryLow = state.getBatteryLow();
if (batteryLow != null) {
updateState(CHANNEL_BATTERY_LOW, batteryLow);
}
updateDeviceProperties(state);
logger.debug(
"Updating thing for sensorId {} to state {batteryLevel: {}, batteryLow: {}, firmwareVersion: {}, modelId: {}, vendor: {}}",
state.getDeviceId(), batteryLevel, batteryLow, state.getFirmwareVersion(), state.getModelId(),
state.getVendor());
}
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
if (active) {
if (command instanceof RefreshType) {
logger.debug("Refreshing channel {}", channelUID);
coapClient.asyncGet(this);
return;
}
logger.debug("The sensor is a read-only device and cannot handle commands.");
}
}
}

View File

@@ -0,0 +1,157 @@
/**
* 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.tradfri.internal.handler;
import static org.openhab.core.thing.Thing.*;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.concurrent.TimeUnit;
import org.eclipse.californium.core.CoapObserveRelation;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.tradfri.internal.CoapCallback;
import org.openhab.binding.tradfri.internal.TradfriCoapClient;
import org.openhab.binding.tradfri.internal.config.TradfriDeviceConfig;
import org.openhab.binding.tradfri.internal.model.TradfriDeviceData;
import org.openhab.core.thing.Bridge;
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.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link TradfriThingHandler} is the abstract base class for individual device handlers.
*
* @author Kai Kreuzer - Initial contribution
* @author Christoph Weitkamp - Restructuring and refactoring of the binding
*/
@NonNullByDefault
public abstract class TradfriThingHandler extends BaseThingHandler implements CoapCallback {
private final Logger logger = LoggerFactory.getLogger(TradfriThingHandler.class);
// the unique instance id of the device
protected @Nullable Integer id;
// used to check whether we have already been disposed when receiving data asynchronously
protected volatile boolean active;
protected @NonNullByDefault({}) TradfriCoapClient coapClient;
private @Nullable CoapObserveRelation observeRelation;
public TradfriThingHandler(Thing thing) {
super(thing);
}
@Override
@SuppressWarnings("null")
public synchronized void initialize() {
Bridge tradfriGateway = getBridge();
this.id = getConfigAs(TradfriDeviceConfig.class).id;
TradfriGatewayHandler handler = (TradfriGatewayHandler) tradfriGateway.getHandler();
String uriString = handler.getGatewayURI() + "/" + id;
try {
URI uri = new URI(uriString);
coapClient = new TradfriCoapClient(uri);
coapClient.setEndpoint(handler.getEndpoint());
} catch (URISyntaxException e) {
logger.debug("Illegal device URI `{}`: {}", uriString, e.getMessage());
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
return;
}
active = true;
updateStatus(ThingStatus.UNKNOWN);
switch (tradfriGateway.getStatus()) {
case ONLINE:
scheduler.schedule(() -> {
observeRelation = coapClient.startObserve(this);
}, 3, TimeUnit.SECONDS);
break;
case OFFLINE:
default:
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE,
String.format("Gateway offline '%s'", tradfriGateway.getStatusInfo()));
break;
}
}
@Override
public synchronized void dispose() {
active = false;
if (observeRelation != null) {
observeRelation.reactiveCancel();
observeRelation = null;
}
if (coapClient != null) {
coapClient.shutdown();
}
super.dispose();
}
@Override
@SuppressWarnings("null")
public void setStatus(ThingStatus status, ThingStatusDetail statusDetail) {
if (active && getBridge().getStatus() != ThingStatus.OFFLINE && status != ThingStatus.ONLINE) {
updateStatus(status, statusDetail);
// we are offline and lost our observe relation - let's try to establish the connection in 10 seconds again
scheduler.schedule(() -> {
if (observeRelation != null) {
observeRelation.reactiveCancel();
observeRelation = null;
}
observeRelation = coapClient.startObserve(this);
}, 10, TimeUnit.SECONDS);
}
}
@Override
public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
super.bridgeStatusChanged(bridgeStatusInfo);
// the status might have changed because the bridge is completely reconfigured - so we need to re-establish
// our CoAP connection as well
if (bridgeStatusInfo.getStatus() == ThingStatus.OFFLINE) {
dispose();
} else if (bridgeStatusInfo.getStatus() == ThingStatus.ONLINE) {
initialize();
}
}
protected void set(String payload) {
logger.debug("Sending payload: {}", payload);
coapClient.asyncPut(payload, this, scheduler);
}
protected void updateDeviceProperties(TradfriDeviceData state) {
String firmwareVersion = state.getFirmwareVersion();
if (firmwareVersion != null) {
getThing().setProperty(PROPERTY_FIRMWARE_VERSION, firmwareVersion);
}
String modelId = state.getModelId();
if (modelId != null) {
getThing().setProperty(PROPERTY_MODEL_ID, modelId);
}
String vendor = state.getVendor();
if (vendor != null) {
getThing().setProperty(PROPERTY_VENDOR, vendor);
}
}
}

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.tradfri.internal.model;
import static org.openhab.binding.tradfri.internal.TradfriBindingConstants.*;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.library.types.PercentType;
import com.google.gson.JsonElement;
import com.google.gson.JsonPrimitive;
/**
* The {@link TradfriBlindData} class is a Java wrapper for the raw JSON data about the blinds state.
*
* @author Manuel Raffel - Initial contribution
*/
@NonNullByDefault
public class TradfriBlindData extends TradfriWirelessDeviceData {
public TradfriBlindData() {
super(BLINDS);
}
public TradfriBlindData(JsonElement json) {
super(BLINDS, json);
}
public TradfriBlindData setPosition(PercentType position) {
attributes.add(POSITION, new JsonPrimitive(position.intValue()));
return this;
}
public TradfriBlindData stop() {
attributes.add(STOP_TRIGGER, new JsonPrimitive(0));
return this;
}
public @Nullable PercentType getPosition() {
PercentType result = null;
JsonElement position = attributes.get(POSITION);
if (position != null) {
int percent = position.getAsInt();
percent = Math.max(percent, 0);
percent = Math.min(100, percent);
result = new PercentType(percent);
}
return result;
}
public String getJsonString() {
return root.toString();
}
}

View File

@@ -0,0 +1,32 @@
/**
* 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.tradfri.internal.model;
import static org.openhab.binding.tradfri.internal.TradfriBindingConstants.SWITCH;
import org.eclipse.jdt.annotation.NonNullByDefault;
import com.google.gson.JsonElement;
/**
* The {@link TradfriControllerData} class is a Java wrapper for the raw JSON data about the controller state.
*
* @author Christoph Weitkamp - Initial contribution
*/
@NonNullByDefault
public class TradfriControllerData extends TradfriWirelessDeviceData {
public TradfriControllerData(JsonElement json) {
super(SWITCH, json);
}
}

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.tradfri.internal.model;
import static org.openhab.binding.tradfri.internal.TradfriBindingConstants.*;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonSyntaxException;
/**
* The {@link TradfriDeviceData} class is a Java wrapper for the raw JSON data about the device state.
*
* @author Kai Kreuzer - Initial contribution
* @author Christoph Weitkamp - Restructuring and refactoring of the binding
*/
@NonNullByDefault
public abstract class TradfriDeviceData {
private final Logger logger = LoggerFactory.getLogger(TradfriDeviceData.class);
protected JsonObject root;
protected JsonArray array;
protected JsonObject attributes;
protected JsonObject generalInfo;
public TradfriDeviceData(String attributesNodeName) {
root = new JsonObject();
array = new JsonArray();
attributes = new JsonObject();
array.add(attributes);
root.add(attributesNodeName, array);
generalInfo = new JsonObject();
root.add(DEVICE, generalInfo);
}
public TradfriDeviceData(String attributesNodeName, JsonElement json) {
try {
root = json.getAsJsonObject();
if (root.has(attributesNodeName)) {
array = root.getAsJsonArray(attributesNodeName);
attributes = array.get(0).getAsJsonObject();
} else {
array = new JsonArray();
attributes = new JsonObject();
array.add(attributes);
}
generalInfo = root.getAsJsonObject(DEVICE);
} catch (JsonSyntaxException e) {
logger.warn("JSON error: {}", e.getMessage(), e);
throw new IllegalArgumentException(e);
}
}
public Integer getDeviceId() {
return root.get(INSTANCE_ID).getAsInt();
}
public boolean getReachabilityStatus() {
if (root.get(REACHABILITY_STATE) != null) {
return root.get(REACHABILITY_STATE).getAsInt() == 1;
} else {
return false;
}
}
public @Nullable String getFirmwareVersion() {
if (generalInfo.get(DEVICE_FIRMWARE) != null) {
return generalInfo.get(DEVICE_FIRMWARE).getAsString();
} else {
return null;
}
}
public @Nullable String getModelId() {
if (generalInfo.get(DEVICE_MODEL) != null) {
return generalInfo.get(DEVICE_MODEL).getAsString();
} else {
return null;
}
}
public @Nullable String getVendor() {
if (generalInfo.get(DEVICE_VENDOR) != null) {
return generalInfo.get(DEVICE_VENDOR).getAsString();
} else {
return null;
}
}
}

View File

@@ -0,0 +1,138 @@
/**
* 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.tradfri.internal.model;
import static org.openhab.binding.tradfri.internal.TradfriBindingConstants.*;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.tradfri.internal.TradfriColor;
import org.openhab.core.library.types.HSBType;
import org.openhab.core.library.types.PercentType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.JsonElement;
import com.google.gson.JsonPrimitive;
/**
* The {@link TradfriLightData} class is a Java wrapper for the raw JSON data about the light state.
*
* @author Kai Kreuzer - Initial contribution
* @author Holger Reichert - Support for color bulbs
* @author Christoph Weitkamp - Restructuring and refactoring of the binding
*/
@NonNullByDefault
public class TradfriLightData extends TradfriDeviceData {
private final Logger logger = LoggerFactory.getLogger(TradfriLightData.class);
public TradfriLightData() {
super(LIGHT);
}
public TradfriLightData(JsonElement json) {
super(LIGHT, json);
}
public TradfriLightData setBrightness(PercentType brightness) {
attributes.add(DIMMER, new JsonPrimitive((int) Math.floor(brightness.doubleValue() * 2.54)));
return this;
}
public @Nullable PercentType getBrightness() {
PercentType result = null;
JsonElement dimmer = attributes.get(DIMMER);
if (dimmer != null) {
result = TradfriColor.xyBrightnessToPercentType(dimmer.getAsInt());
}
return result;
}
public TradfriLightData setTransitionTime(int seconds) {
attributes.add(TRANSITION_TIME, new JsonPrimitive(seconds));
return this;
}
public int getTransitionTime() {
JsonElement transitionTime = attributes.get(TRANSITION_TIME);
if (transitionTime != null) {
return transitionTime.getAsInt();
} else {
return 0;
}
}
public TradfriLightData setColorTemperature(PercentType c) {
TradfriColor color = new TradfriColor(c);
int x = color.xyX;
int y = color.xyY;
logger.debug("New color temperature: {},{} ({} %)", x, y, c.intValue());
attributes.add(COLOR_X, new JsonPrimitive(x));
attributes.add(COLOR_Y, new JsonPrimitive(y));
return this;
}
public @Nullable PercentType getColorTemperature() {
JsonElement colorX = attributes.get(COLOR_X);
JsonElement colorY = attributes.get(COLOR_Y);
if (colorX != null && colorY != null) {
TradfriColor color = new TradfriColor(colorX.getAsInt(), colorY.getAsInt(), null);
return color.getColorTemperature();
} else {
return null;
}
}
public TradfriLightData setColor(HSBType hsb) {
TradfriColor color = new TradfriColor(hsb);
attributes.add(COLOR_X, new JsonPrimitive(color.xyX));
attributes.add(COLOR_Y, new JsonPrimitive(color.xyY));
return this;
}
public @Nullable HSBType getColor() {
// XY color coordinates plus brightness is needed for color calculation
JsonElement colorX = attributes.get(COLOR_X);
JsonElement colorY = attributes.get(COLOR_Y);
JsonElement dimmer = attributes.get(DIMMER);
if (colorX != null && colorY != null && dimmer != null) {
int x = colorX.getAsInt();
int y = colorY.getAsInt();
int brightness = dimmer.getAsInt();
TradfriColor color = new TradfriColor(x, y, brightness);
return color.getHSB();
}
return null;
}
public TradfriLightData setOnOffState(boolean on) {
attributes.add(ONOFF, new JsonPrimitive(on ? 1 : 0));
return this;
}
public boolean getOnOffState() {
JsonElement onOff = attributes.get(ONOFF);
if (onOff != null) {
return onOff.getAsInt() == 1;
} else {
return false;
}
}
public String getJsonString() {
return root.toString();
}
}

View File

@@ -0,0 +1,69 @@
/**
* 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.tradfri.internal.model;
import static org.openhab.binding.tradfri.internal.TradfriBindingConstants.*;
import org.eclipse.jdt.annotation.NonNullByDefault;
import com.google.gson.JsonElement;
import com.google.gson.JsonPrimitive;
/**
* The {@link TradfriPlugData} class is a Java wrapper for the raw JSON data about the plug state.
*
* @author Kai Kreuzer - Initial contribution
*/
@NonNullByDefault
public class TradfriPlugData extends TradfriDeviceData {
public TradfriPlugData() {
super(PLUG);
}
public TradfriPlugData(JsonElement json) {
super(PLUG, json);
}
public TradfriPlugData setTransitionTime(int seconds) {
attributes.add(TRANSITION_TIME, new JsonPrimitive(seconds));
return this;
}
public int getTransitionTime() {
JsonElement transitionTime = attributes.get(TRANSITION_TIME);
if (transitionTime != null) {
return transitionTime.getAsInt();
} else {
return 0;
}
}
public TradfriPlugData setOnOffState(boolean on) {
attributes.add(ONOFF, new JsonPrimitive(on ? 1 : 0));
return this;
}
public boolean getOnOffState() {
JsonElement onOff = attributes.get(ONOFF);
if (onOff != null) {
return onOff.getAsInt() == 1;
} else {
return false;
}
}
public String getJsonString() {
return root.toString();
}
}

View File

@@ -0,0 +1,32 @@
/**
* 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.tradfri.internal.model;
import static org.openhab.binding.tradfri.internal.TradfriBindingConstants.SENSOR;
import org.eclipse.jdt.annotation.NonNullByDefault;
import com.google.gson.JsonElement;
/**
* The {@link TradfriSensorData} class is a Java wrapper for the raw JSON data about the sensor state.
*
* @author Christoph Weitkamp - Initial contribution
*/
@NonNullByDefault
public class TradfriSensorData extends TradfriWirelessDeviceData {
public TradfriSensorData(JsonElement json) {
super(SENSOR, json);
}
}

View File

@@ -0,0 +1,90 @@
/**
* 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.tradfri.internal.model;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* The {@link TradfriVersion} class is a default implementation for comparing TRÅDFRI versions.
*
* @author Christoph Weitkamp - Initial contribution
*/
@NonNullByDefault
public class TradfriVersion implements Comparable<TradfriVersion> {
private static final String VERSION_PATTERN = "[0-9]+(\\.[0-9]+)*";
private static final String VERSION_DELIMITER = "\\.";
final List<Integer> parts;
/**
* Create a new instance.
*
* @param version the version string
*/
public TradfriVersion(final String version) {
if (!version.matches(VERSION_PATTERN)) {
throw new IllegalArgumentException("TradfriVersion cannot be created as version has invalid format.");
}
parts = Arrays.stream(version.split(VERSION_DELIMITER)).map(part -> Integer.parseInt(part))
.collect(Collectors.toList());
}
@Override
public int compareTo(final TradfriVersion other) {
int minSize = Math.min(parts.size(), other.parts.size());
for (int i = 0; i < minSize; ++i) {
int diff = parts.get(i) - other.parts.get(i);
if (diff == 0) {
continue;
} else if (diff < 0) {
return -1;
} else {
return 1;
}
}
for (int i = minSize; i < parts.size(); ++i) {
if (parts.get(i) != 0) {
return 1;
}
}
for (int i = minSize; i < other.parts.size(); ++i) {
if (other.parts.get(i) != 0) {
return -1;
}
}
return 0;
}
@Override
public boolean equals(@Nullable Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
return compareTo((TradfriVersion) obj) == 0;
}
@Override
public String toString() {
return parts.stream().map(String::valueOf).collect(Collectors.joining("."));
}
}

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.tradfri.internal.model;
import static org.openhab.binding.tradfri.internal.TradfriBindingConstants.DEVICE_BATTERY_LEVEL;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.OnOffType;
import com.google.gson.JsonElement;
/**
* The {@link TradfriWirelessDeviceData} class is a Java wrapper for the raw JSON data about wireless device state.
*
* @author Christoph Weitkamp - Initial contribution
*/
@NonNullByDefault
public abstract class TradfriWirelessDeviceData extends TradfriDeviceData {
public TradfriWirelessDeviceData(String attributesNodeName) {
super(attributesNodeName);
}
public TradfriWirelessDeviceData(String attributesNodeName, JsonElement json) {
super(attributesNodeName, json);
}
public @Nullable DecimalType getBatteryLevel() {
if (generalInfo.get(DEVICE_BATTERY_LEVEL) != null) {
return new DecimalType(generalInfo.get(DEVICE_BATTERY_LEVEL).getAsInt());
} else {
return null;
}
}
public @Nullable OnOffType getBatteryLow() {
if (generalInfo.get(DEVICE_BATTERY_LEVEL) != null) {
return generalInfo.get(DEVICE_BATTERY_LEVEL).getAsInt() <= 10 ? OnOffType.ON : OnOffType.OFF;
} else {
return null;
}
}
}

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<binding:binding id="tradfri" 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>TRÅDFRI Binding</name>
<description>This binding supports IKEA TRÅDFRI lighting devices through the IKEA gateway.</description>
</binding:binding>

View File

@@ -0,0 +1,44 @@
<?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="bridge-type:tradfri:gateway">
<parameter name="host" type="text" required="true">
<context>network-address</context>
<label>Host</label>
<description>Hostname or IP address of the IKEA TRÅDFRI gateway</description>
</parameter>
<parameter name="port" type="integer" required="false">
<label>Port</label>
<description>Port for accessing the gateway</description>
<advanced>true</advanced>
<default>5684</default>
</parameter>
<parameter name="code" type="text" required="false">
<context>password</context>
<label>Security Code</label>
<description>Security code printed on the label underneath the gateway.</description>
</parameter>
<parameter name="identity" type="text" required="false">
<advanced>true</advanced>
<label>Identity</label>
<description>Unique identity used for communication with the gateway</description>
</parameter>
<parameter name="preSharedKey" type="text" required="false">
<advanced>true</advanced>
<context>password</context>
<label>Pre-Shared Security Key</label>
<description>Security key obtained during first initialization of the gateway</description>
</parameter>
</config-description>
<config-description uri="thing-type:tradfri:device">
<parameter name="id" type="integer" required="true">
<label>ID</label>
<description>The identifier of the device on the gateway.</description>
</parameter>
</config-description>
</config-description:config-descriptions>

View File

@@ -0,0 +1,47 @@
# binding
binding.tradfri.name = TRÅDFRI Binding
binding.tradfri.description = Dieses Binding integriert das IKEA TRÅDFRI System. Durch dieses können die TRÅDFRI Lampen und Leuchten gesteuert werden.
# bridge types
thing-type.tradfri.gateway.label = TRÅDFRI Gateway
thing-type.tradfri.gateway.description = IKEA TRÅDFRI Gateway/zentrale Steuereinheit.
# bridge type configuration
bridge-type.config.tradfri.gateway.host.label = IP-Adresse
bridge-type.config.tradfri.gateway.host.description = Lokale IP-Adresse oder Hostname des TRÅDFRI Gateway.
bridge-type.config.tradfri.gateway.port.label = Port
bridge-type.config.tradfri.gateway.port.description = Port des TRÅDFRI Gateway.
bridge-type.config.tradfri.gateway.code.label = Security Code
bridge-type.config.tradfri.gateway.code.description = Security Code zur Authentifizierung am TRÅDFRI Gateway. Befindet sich unterhalb des TRÅDFRI Gateway.
# thing types
thing-type.tradfri.0100.label = Dimmbare Lampe (weiß)
thing-type.tradfri.0100.description = Dimmbare Lampe mit fester Farbtemperatur.
thing-type.tradfri.0210.label = Farbspektrum Lampe
thing-type.tradfri.0210.description = Dimmbare Lampe mit einstellbarer Farbe und Farbtemperatur.
thing-type.tradfri.0220.label = Farbtemperatur Lampe (weiß)
thing-type.tradfri.0220.description = Dimmbare Lampe mit einstellbarer Farbtemperatur.
thing-type.tradfri.0107.label = Funk-Bewegungsmelder
thing-type.tradfri.0107.description = Der Funk-Bewegungsmelder liefert Daten wie z.B. die Batterieladung.
thing-type.tradfri.0820.label = Kabelloser Dimmer
thing-type.tradfri.0820.description = Der Kabellose Dimmer liefert Daten wie z.B. die Batterieladung.
thing-type.tradfri.0830.label = Fernbedienung
thing-type.tradfri.0830.description = Die Fernbedienung liefert Daten wie z.B. die Batterieladung.
thing-type.tradfri.0202.label = Rollo
thing-type.tradfri.0202.description = Batteriebetriebenes Rollo mit einstellbarer Position. Liefert außerdem Daten wie z.B. die Batterieladung.
thing-type.tradfri.0203.label = Rollo-Fernbedienung
thing-type.tradfri.0203.description = Die Rollo-Fernbedienung liefert Daten wie z.B. die Batterieladung.
# thing types config
thing-type.config.tradfri.device.id.label = ID des Gerätes
thing-type.config.tradfri.device.id.description = ID zur Identifikation des Gerätes.
# channel types
channel-type.tradfri.brightness.label = Helligkeit
channel-type.tradfri.brightness.description = Ermöglicht die Steuerung der Helligkeit. Ermöglicht ebenfalls die Lampe ein- und auszuschalten.
channel-type.tradfri.color_temperature.label = Farbtemperatur
channel-type.tradfri.color_temperature.description = Ermöglicht die Steuerung der Farbtemperatur. Von Tageslichtweiß (0) bis Warmweiß (100).
channel-type.tradfri.color.label = Farbe
channel-type.tradfri.color.description = Ermöglicht die Steuerung der Farbe.
channel-type.tradfri.position.label = Position
channel-type.tradfri.position.description = Ermöglicht die Steuerung der Position von Offen (0) bis Geschlossen (100).

View File

@@ -0,0 +1,210 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="tradfri"
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="gateway">
<label>TRÅDFRI Gateway</label>
<description>IKEA TRÅDFRI IP Gateway</description>
<representation-property>host</representation-property>
<config-description-ref uri="bridge-type:tradfri:gateway"/>
</bridge-type>
<!-- thing types for devices -->
<!-- their IDs refer to the Zigbee Lightlink device ids (see chapter 2.2 in https://www.nxp.com/documents/user_manual/JN-UG-3091.pdf) -->
<thing-type id="0010">
<supported-bridge-type-refs>
<bridge-type-ref id="gateway"/>
</supported-bridge-type-refs>
<label>On/Off Plug</label>
<description>A plug that can be switched on and off.</description>
<channels>
<channel id="power" typeId="system.power"/>
</channels>
<representation-property>id</representation-property>
<config-description-ref uri="thing-type:tradfri:device"/>
</thing-type>
<thing-type id="0100">
<supported-bridge-type-refs>
<bridge-type-ref id="gateway"/>
</supported-bridge-type-refs>
<label>Dimmable Light</label>
<description>A light that has continuous brightness control.</description>
<channels>
<channel id="brightness" typeId="brightness"/>
</channels>
<representation-property>id</representation-property>
<config-description-ref uri="thing-type:tradfri:device"/>
</thing-type>
<thing-type id="0220">
<supported-bridge-type-refs>
<bridge-type-ref id="gateway"/>
</supported-bridge-type-refs>
<label>Color Temperature Light</label>
<description>A dimmable light that supports different color temperature settings.</description>
<channels>
<channel id="brightness" typeId="brightness"/>
<channel id="color_temperature" typeId="color_temperature"/>
</channels>
<representation-property>id</representation-property>
<config-description-ref uri="thing-type:tradfri:device"/>
</thing-type>
<thing-type id="0210">
<supported-bridge-type-refs>
<bridge-type-ref id="gateway"/>
</supported-bridge-type-refs>
<label>Color Light</label>
<description>A dimmable light that supports full colors and color temperature settings.</description>
<channels>
<channel id="color_temperature" typeId="color_temperature"/>
<channel id="color" typeId="color"/>
</channels>
<representation-property>id</representation-property>
<config-description-ref uri="thing-type:tradfri:device"/>
</thing-type>
<thing-type id="0107">
<supported-bridge-type-refs>
<bridge-type-ref id="gateway"/>
</supported-bridge-type-refs>
<label>Occupancy Sensor</label>
<description>This represents the motion sensor capable of reporting the battery level.</description>
<channels>
<channel id="battery_level" typeId="system.battery-level"/>
<channel id="battery_low" typeId="system.low-battery"/>
</channels>
<representation-property>id</representation-property>
<config-description-ref uri="thing-type:tradfri:device"/>
</thing-type>
<thing-type id="0820" listed="false">
<supported-bridge-type-refs>
<bridge-type-ref id="gateway"/>
</supported-bridge-type-refs>
<label>Non-Colour Controller</label>
<description>This represents the wireless dimmer sensor capable of reporting the battery level.</description>
<channels>
<channel id="battery_level" typeId="system.battery-level"/>
<channel id="battery_low" typeId="system.low-battery"/>
</channels>
<representation-property>id</representation-property>
<config-description-ref uri="thing-type:tradfri:device"/>
</thing-type>
<thing-type id="0830">
<supported-bridge-type-refs>
<bridge-type-ref id="gateway"/>
</supported-bridge-type-refs>
<label>Non-Colour Scene Controller</label>
<description>This represents the remote control capable of reporting the battery level.</description>
<channels>
<channel id="battery_level" typeId="system.battery-level"/>
<channel id="battery_low" typeId="system.low-battery"/>
</channels>
<representation-property>id</representation-property>
<config-description-ref uri="thing-type:tradfri:device"/>
</thing-type>
<thing-type id="0202">
<supported-bridge-type-refs>
<bridge-type-ref id="gateway"/>
</supported-bridge-type-refs>
<label>Window Covering Device</label>
<description>This represents a blind or curtain that can be moved up and down. Also reports current battery level.</description>
<channels>
<channel id="position" typeId="position"/>
<channel id="battery_level" typeId="system.battery-level"/>
<channel id="battery_low" typeId="system.low-battery"/>
</channels>
<representation-property>id</representation-property>
<config-description-ref uri="thing-type:tradfri:device"/>
</thing-type>
<thing-type id="0203" listed="false">
<supported-bridge-type-refs>
<bridge-type-ref id="gateway"/>
</supported-bridge-type-refs>
<label>Window Covering Controller</label>
<description>This represents the wireless open/close remote capable of reporting the battery level.</description>
<channels>
<channel id="battery_level" typeId="system.battery-level"/>
<channel id="battery_low" typeId="system.low-battery"/>
</channels>
<representation-property>id</representation-property>
<config-description-ref uri="thing-type:tradfri:device"/>
</thing-type>
<channel-type id="brightness">
<item-type>Dimmer</item-type>
<label>Brightness</label>
<description>Control the brightness and switch the light on and off.</description>
<category>DimmableLight</category>
<tags>
<tag>Lighting</tag>
</tags>
</channel-type>
<channel-type id="position">
<item-type>Rollershutter</item-type>
<label>Position</label>
<description>Control the position of the blind or curtain in percent from 0 (open) to 100 (closed).</description>
<category>Blinds</category>
</channel-type>
<channel-type id="color_temperature">
<item-type>Dimmer</item-type>
<label>Color Temperature</label>
<description>Control the color temperature of the light.</description>
<category>ColorLight</category>
</channel-type>
<channel-type id="color">
<item-type>Color</item-type>
<label>Color</label>
<description>Control the color of the light.</description>
<category>ColorLight</category>
</channel-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,189 @@
/**
* 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.tradfri.internal;
import static org.junit.Assert.*;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.Test;
import org.openhab.core.library.types.HSBType;
import org.openhab.core.library.types.PercentType;
/**
* Tests for {@link TradfriColor}.
*
* @author Holger Reichert - Initial contribution
*/
@NonNullByDefault
public class TradfriColorTest {
@Test
public void testFromCieKnownGood1() {
TradfriColor color = new TradfriColor(29577, 12294, 354);
assertEquals(29577, (int) color.xyX);
assertEquals(12294, (int) color.xyY);
assertEquals(254, (int) color.brightness);
HSBType hsbType = color.getHSB();
assertNotNull(hsbType);
assertEquals(321, hsbType.getHue().intValue());
assertEquals(100, hsbType.getSaturation().intValue());
assertEquals(100, hsbType.getBrightness().intValue());
}
@Test
public void testFromCieKnownGood2() {
TradfriColor color = new TradfriColor(19983, 37417, 84);
assertEquals(19983, (int) color.xyX);
assertEquals(37417, (int) color.xyY);
assertEquals(84, (int) color.brightness);
HSBType hsbType = color.getHSB();
assertNotNull(hsbType);
assertEquals(115, hsbType.getHue().intValue());
assertEquals(77, hsbType.getSaturation().intValue());
assertEquals(34, hsbType.getBrightness().intValue());
}
@Test
public void testFromCieKnownGood3() {
TradfriColor color = new TradfriColor(19983, 37417, 1);
assertEquals(19983, (int) color.xyX);
assertEquals(37417, (int) color.xyY);
assertEquals(1, (int) color.brightness);
HSBType hsbType = color.getHSB();
assertNotNull(hsbType);
assertEquals(115, hsbType.getHue().intValue());
assertEquals(77, hsbType.getSaturation().intValue());
assertEquals(1, hsbType.getBrightness().intValue());
}
@Test
public void testFromCieKnownGood4() {
TradfriColor color = new TradfriColor(11413, 31334, 181);
assertEquals(11413, (int) color.xyX);
assertEquals(31334, (int) color.xyY);
assertEquals(181, (int) color.brightness);
HSBType hsbType = color.getHSB();
assertNotNull(hsbType);
assertEquals(158, hsbType.getHue().intValue());
assertEquals(100, hsbType.getSaturation().intValue());
assertEquals(72, hsbType.getBrightness().intValue());
}
@Test
public void testFromHSBTypeKnownGood1() {
TradfriColor color = new TradfriColor(HSBType.RED);
assertEquals(41947, (int) color.xyX);
assertEquals(21625, (int) color.xyY);
assertEquals(254, (int) color.brightness);
HSBType hsbType = color.getHSB();
assertNotNull(hsbType);
assertEquals(0, hsbType.getHue().intValue());
assertEquals(100, hsbType.getSaturation().intValue());
assertEquals(100, hsbType.getBrightness().intValue());
}
@Test
public void testFromHSBTypeKnownGood2() {
TradfriColor color = new TradfriColor(new HSBType("0,100,1"));
assertEquals(41947, (int) color.xyX);
assertEquals(21625, (int) color.xyY);
assertEquals(2, (int) color.brightness);
HSBType hsbType = color.getHSB();
assertNotNull(hsbType);
assertEquals(0, hsbType.getHue().intValue());
assertEquals(100, hsbType.getSaturation().intValue());
assertEquals(1, hsbType.getBrightness().intValue());
}
@Test
public void testConversionReverse() {
// convert from HSBType
TradfriColor color = new TradfriColor(HSBType.GREEN);
assertEquals(19660, (int) color.xyX);
assertEquals(39321, (int) color.xyY);
assertEquals(254, (int) color.brightness);
HSBType hsbType = color.getHSB();
assertNotNull(hsbType);
assertEquals(120, hsbType.getHue().intValue());
assertEquals(100, hsbType.getSaturation().intValue());
assertEquals(100, hsbType.getBrightness().intValue());
// convert the result again based on the XY values
TradfriColor reverse = new TradfriColor(color.xyX, color.xyY, color.brightness);
assertEquals(19660, (int) reverse.xyX);
assertEquals(39321, (int) reverse.xyY);
assertEquals(254, (int) reverse.brightness);
HSBType hsbTypeReverse = color.getHSB();
assertNotNull(hsbTypeReverse);
assertEquals(120, hsbTypeReverse.getHue().intValue());
assertEquals(100, hsbTypeReverse.getSaturation().intValue());
assertEquals(100, hsbTypeReverse.getBrightness().intValue());
}
@Test
public void testFromColorTemperatureMinMiddleMax() {
// coldest color temperature -> preset 1
TradfriColor colorMin = new TradfriColor(PercentType.ZERO);
assertNotNull(colorMin);
assertEquals(24933, (int) colorMin.xyX);
assertEquals(24691, (int) colorMin.xyY);
// middle color temperature -> preset 2
TradfriColor colorMiddle = new TradfriColor(new PercentType(50));
assertNotNull(colorMiddle);
assertEquals(30138, (int) colorMiddle.xyX);
assertEquals(26909, (int) colorMiddle.xyY);
// warmest color temperature -> preset 3
TradfriColor colorMax = new TradfriColor(PercentType.HUNDRED);
assertNotNull(colorMax);
assertEquals(33137, (int) colorMax.xyX);
assertEquals(27211, (int) colorMax.xyY);
}
@Test
public void testFromColorTemperatureInbetween() {
// 30 percent must be between preset 1 and 2
TradfriColor color2 = new TradfriColor(new PercentType(30));
assertNotNull(color2);
assertEquals(28056, (int) color2.xyX);
assertEquals(26022, (int) color2.xyY);
// 70 percent must be between preset 2 and 3
TradfriColor color3 = new TradfriColor(new PercentType(70));
assertNotNull(color3);
assertEquals(31338, (int) color3.xyX);
assertEquals(27030, (int) color3.xyY);
}
@Test
public void testCalculateColorTemperature() {
// preset 1 -> coldest -> 0 percent
PercentType preset1 = new TradfriColor(24933, 24691, null).getColorTemperature();
assertEquals(0, preset1.intValue());
// preset 2 -> middle -> 50 percent
PercentType preset2 = new TradfriColor(30138, 26909, null).getColorTemperature();
assertEquals(50, preset2.intValue());
// preset 3 -> warmest -> 100 percent
PercentType preset3 = new TradfriColor(33137, 27211, null).getColorTemperature();
assertEquals(100, preset3.intValue());
// preset 3 -> warmest -> 100 percent
PercentType colder = new TradfriColor(22222, 23333, null).getColorTemperature();
assertEquals(0, colder.intValue());
// preset 3 -> warmest -> 100 percent
PercentType temp3 = new TradfriColor(34000, 34000, null).getColorTemperature();
assertEquals(100, temp3.intValue());
// mixed case 1
PercentType mixed1 = new TradfriColor(0, 1000000, null).getColorTemperature();
assertEquals(0, mixed1.intValue());
// mixed case 1
PercentType mixed2 = new TradfriColor(1000000, 0, null).getColorTemperature();
assertEquals(100, mixed2.intValue());
}
}

View File

@@ -0,0 +1,233 @@
/**
* 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.tradfri.internal.discovery;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.*;
import static org.mockito.Mockito.when;
import static org.mockito.MockitoAnnotations.initMocks;
import static org.openhab.binding.tradfri.internal.TradfriBindingConstants.*;
import static org.openhab.binding.tradfri.internal.config.TradfriDeviceConfig.CONFIG_ID;
import java.util.Collection;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mock;
import org.openhab.binding.tradfri.internal.handler.TradfriGatewayHandler;
import org.openhab.core.config.discovery.DiscoveryListener;
import org.openhab.core.config.discovery.DiscoveryResult;
import org.openhab.core.config.discovery.DiscoveryResultFlag;
import org.openhab.core.config.discovery.DiscoveryService;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.thing.binding.builder.BridgeBuilder;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
/**
* Tests for {@link TradfriDiscoveryService}.
*
* @author Kai Kreuzer - Initial contribution
* @author Christoph Weitkamp - Added support for remote controller and motion sensor devices (read-only battery level)
*/
public class TradfriDiscoveryServiceTest {
private static final ThingUID GATEWAY_THING_UID = new ThingUID("tradfri:gateway:1");
@Mock
private TradfriGatewayHandler handler;
private final DiscoveryListener listener = new DiscoveryListener() {
@Override
public void thingRemoved(DiscoveryService source, ThingUID thingUID) {
}
@Override
public void thingDiscovered(DiscoveryService source, DiscoveryResult result) {
discoveryResult = result;
}
@Override
public Collection<ThingUID> removeOlderResults(DiscoveryService source, long timestamp,
Collection<ThingTypeUID> thingTypeUIDs, ThingUID bridgeUID) {
return null;
}
};
private DiscoveryResult discoveryResult;
private TradfriDiscoveryService discovery;
@Before
public void setUp() {
initMocks(this);
when(handler.getThing()).thenReturn(BridgeBuilder.create(GATEWAY_TYPE_UID, "1").build());
discovery = new TradfriDiscoveryService();
discovery.setThingHandler(handler);
discovery.addDiscoveryListener(listener);
}
@After
public void cleanUp() {
discoveryResult = null;
}
@Test
public void correctSupportedTypes() {
assertThat(discovery.getSupportedThingTypes().size(), is(9));
assertTrue(discovery.getSupportedThingTypes().contains(THING_TYPE_DIMMABLE_LIGHT));
assertTrue(discovery.getSupportedThingTypes().contains(THING_TYPE_COLOR_TEMP_LIGHT));
assertTrue(discovery.getSupportedThingTypes().contains(THING_TYPE_COLOR_LIGHT));
assertTrue(discovery.getSupportedThingTypes().contains(THING_TYPE_DIMMER));
assertTrue(discovery.getSupportedThingTypes().contains(THING_TYPE_MOTION_SENSOR));
assertTrue(discovery.getSupportedThingTypes().contains(THING_TYPE_REMOTE_CONTROL));
assertTrue(discovery.getSupportedThingTypes().contains(THING_TYPE_OPEN_CLOSE_REMOTE_CONTROL));
assertTrue(discovery.getSupportedThingTypes().contains(THING_TYPE_ONOFF_PLUG));
assertTrue(discovery.getSupportedThingTypes().contains(THING_TYPE_BLINDS));
}
@Test
public void validDiscoveryResultWhiteLightW() {
String json = "{\"9001\":\"TRADFRI bulb E27 W opal 1000lm\",\"9002\":1492856270,\"9020\":1507194357,\"9003\":65537,\"3311\":[{\"5850\":1,\"5851\":254,\"9003\":0}],\"9054\":0,\"5750\":2,\"9019\":1,\"3\":{\"0\":\"IKEA of Sweden\",\"1\":\"TRADFRI bulb E27 W opal 1000lm\",\"2\":\"\",\"3\":\"1.2.214\",\"6\":1}}";
JsonObject data = new JsonParser().parse(json).getAsJsonObject();
discovery.onUpdate("65537", data);
assertNotNull(discoveryResult);
assertThat(discoveryResult.getFlag(), is(DiscoveryResultFlag.NEW));
assertThat(discoveryResult.getThingUID(), is(new ThingUID("tradfri:0100:1:65537")));
assertThat(discoveryResult.getThingTypeUID(), is(THING_TYPE_DIMMABLE_LIGHT));
assertThat(discoveryResult.getBridgeUID(), is(GATEWAY_THING_UID));
assertThat(discoveryResult.getProperties().get(CONFIG_ID), is(65537));
assertThat(discoveryResult.getRepresentationProperty(), is(CONFIG_ID));
}
@Test
public void validDiscoveryResultWhiteLightWS() {
String json = "{\"9001\":\"TRADFRI bulb E27 WS opal 980lm\",\"9002\":1492955148,\"9020\":1507200447,\"9003\":65537,\"3311\":[{\"5710\":26909,\"5850\":1,\"5851\":203,\"5707\":0,\"5708\":0,\"5709\":30140,\"5711\":370,\"5706\":\"f1e0b5\",\"9003\":0}],\"9054\":0,\"5750\":2,\"9019\":1,\"3\":{\"0\":\"IKEA of Sweden\",\"1\":\"TRADFRI bulb E27 WS opal 980lm\",\"2\":\"\",\"3\":\"1.2.217\",\"6\":1}}";
JsonObject data = new JsonParser().parse(json).getAsJsonObject();
discovery.onUpdate("65537", data);
assertNotNull(discoveryResult);
assertThat(discoveryResult.getFlag(), is(DiscoveryResultFlag.NEW));
assertThat(discoveryResult.getThingUID(), is(new ThingUID("tradfri:0220:1:65537")));
assertThat(discoveryResult.getThingTypeUID(), is(THING_TYPE_COLOR_TEMP_LIGHT));
assertThat(discoveryResult.getBridgeUID(), is(GATEWAY_THING_UID));
assertThat(discoveryResult.getProperties().get(CONFIG_ID), is(65537));
assertThat(discoveryResult.getRepresentationProperty(), is(CONFIG_ID));
}
@Test
public void validDiscoveryResultWhiteLightWSWithIncompleteJson() {
// We do not always receive a COLOR = "5706" attribute, even the light supports it - but the gateway does not
// seem to have this information, if the bulb is unreachable.
String json = "{\"9001\":\"TRADFRI bulb E27 WS opal 980lm\",\"9002\":1492955148,\"9020\":1506968670,\"9003\":65537,\"3311\":[{\"9003\":0}],\"9054\":0,\"5750\":2,\"9019\":0,\"3\":{\"0\":\"IKEA of Sweden\",\"1\":\"TRADFRI bulb E27 WS opal 980lm\",\"2\":\"\",\"3\":\"1.2.217\",\"6\":1}}";
JsonObject data = new JsonParser().parse(json).getAsJsonObject();
discovery.onUpdate("65537", data);
assertNotNull(discoveryResult);
assertThat(discoveryResult.getFlag(), is(DiscoveryResultFlag.NEW));
assertThat(discoveryResult.getThingUID(), is(new ThingUID("tradfri:0220:1:65537")));
assertThat(discoveryResult.getThingTypeUID(), is(THING_TYPE_COLOR_TEMP_LIGHT));
assertThat(discoveryResult.getBridgeUID(), is(GATEWAY_THING_UID));
assertThat(discoveryResult.getProperties().get(CONFIG_ID), is(65537));
assertThat(discoveryResult.getRepresentationProperty(), is(CONFIG_ID));
}
@Test
public void validDiscoveryResultColorLightCWS() {
String json = "{\"9001\":\"TRADFRI bulb E27 CWS opal 600lm\",\"9002\":1505151864,\"9020\":1505433527,\"9003\":65550,\"9019\":1,\"9054\":0,\"5750\":2,\"3\":{\"0\":\"IKEA of Sweden\",\"1\":\"TRADFRI bulb E27 CWS opal 600lm\",\"2\":\"\",\"3\":\"1.3.002\",\"6\":1},\"3311\":[{\"5850\":1,\"5708\":0,\"5851\":254,\"5707\":0,\"5709\":33137,\"5710\":27211,\"5711\":0,\"5706\":\"efd275\",\"9003\":0}]}";
JsonObject data = new JsonParser().parse(json).getAsJsonObject();
discovery.onUpdate("65550", data);
assertNotNull(discoveryResult);
assertThat(discoveryResult.getFlag(), is(DiscoveryResultFlag.NEW));
assertThat(discoveryResult.getThingUID(), is(new ThingUID("tradfri:0210:1:65550")));
assertThat(discoveryResult.getThingTypeUID(), is(THING_TYPE_COLOR_LIGHT));
assertThat(discoveryResult.getBridgeUID(), is(GATEWAY_THING_UID));
assertThat(discoveryResult.getProperties().get(CONFIG_ID), is(65550));
assertThat(discoveryResult.getRepresentationProperty(), is(CONFIG_ID));
}
@Test
public void validDiscoveryResultAlternativeColorLightCWS() {
String json = "{\"3311\":[{\"5850\":1,\"5709\":32886,\"5851\":216,\"5707\":5309,\"5708\":52400,\"5710\":27217,\"5706\":\"efd275\",\"9003\":0}],\"9001\":\"Mushroom lamp\",\"9002\":1571036916,\"9020\":1571588312,\"9003\":65539,\"9054\":0,\"9019\":1,\"3\":{\"0\":\"IKEA of Sweden\",\"1\":\"TRADFRI bulb E27 C\\/WS opal 600\",\"2\":\"\",\"3\":\"1.3.009\",\"6\":1},\"5750\":2}";
JsonObject data = new JsonParser().parse(json).getAsJsonObject();
discovery.onUpdate("65539", data);
assertNotNull(discoveryResult);
assertThat(discoveryResult.getFlag(), is(DiscoveryResultFlag.NEW));
assertThat(discoveryResult.getThingUID(), is(new ThingUID("tradfri:0210:1:65539")));
assertThat(discoveryResult.getThingTypeUID(), is(THING_TYPE_COLOR_LIGHT));
assertThat(discoveryResult.getBridgeUID(), is(GATEWAY_THING_UID));
assertThat(discoveryResult.getProperties().get(CONFIG_ID), is(65539));
assertThat(discoveryResult.getRepresentationProperty(), is(CONFIG_ID));
}
@Test
public void validDiscoveryResultRemoteControl() {
String json = "{\"9001\":\"TRADFRI remote control\",\"9002\":1492843083,\"9020\":1506977986,\"9003\":65536,\"9054\":0,\"5750\":0,\"9019\":1,\"3\":{\"0\":\"IKEA of Sweden\",\"1\":\"TRADFRI remote control\",\"2\":\"\",\"3\":\"1.2.214\",\"6\":3,\"9\":47},\"15009\":[{\"9003\":0}]}";
JsonObject data = new JsonParser().parse(json).getAsJsonObject();
discovery.onUpdate("65536", data);
assertNotNull(discoveryResult);
assertThat(discoveryResult.getFlag(), is(DiscoveryResultFlag.NEW));
assertThat(discoveryResult.getThingUID(), is(new ThingUID("tradfri:0830:1:65536")));
assertThat(discoveryResult.getThingTypeUID(), is(THING_TYPE_REMOTE_CONTROL));
assertThat(discoveryResult.getBridgeUID(), is(GATEWAY_THING_UID));
assertThat(discoveryResult.getProperties().get(CONFIG_ID), is(65536));
assertThat(discoveryResult.getRepresentationProperty(), is(CONFIG_ID));
}
@Test
public void validDiscoveryResultWirelessDimmer() {
String json = "{\"9001\":\"TRADFRI wireless dimmer\",\"9002\":1492843083,\"9020\":1506977986,\"9003\":65536,\"9054\":0,\"5750\":0,\"9019\":1,\"3\":{\"0\":\"IKEA of Sweden\",\"1\":\"TRADFRI wireless dimmer\",\"2\":\"\",\"3\":\"1.2.214\",\"6\":3,\"9\":47},\"15009\":[{\"9003\":0}]}";
JsonObject data = new JsonParser().parse(json).getAsJsonObject();
discovery.onUpdate("65536", data);
assertNotNull(discoveryResult);
assertThat(discoveryResult.getFlag(), is(DiscoveryResultFlag.NEW));
assertThat(discoveryResult.getThingUID(), is(new ThingUID("tradfri:0820:1:65536")));
assertThat(discoveryResult.getThingTypeUID(), is(THING_TYPE_DIMMER));
assertThat(discoveryResult.getBridgeUID(), is(GATEWAY_THING_UID));
assertThat(discoveryResult.getProperties().get(CONFIG_ID), is(65536));
assertThat(discoveryResult.getRepresentationProperty(), is(CONFIG_ID));
}
@Test
public void validDiscoveryResultMotionSensor() {
String json = "{\"9001\":\"TRADFRI motion sensor\",\"9002\":1492955083,\"9020\":1507120083,\"9003\":65538,\"9054\":0,\"5750\":4,\"9019\":1,\"3\":{\"0\":\"IKEA of Sweden\",\"1\":\"TRADFRI motion sensor\",\"2\":\"\",\"3\":\"1.2.214\",\"6\":3,\"9\":60},\"3300\":[{\"9003\":0}]}";
JsonObject data = new JsonParser().parse(json).getAsJsonObject();
discovery.onUpdate("65538", data);
assertNotNull(discoveryResult);
assertThat(discoveryResult.getFlag(), is(DiscoveryResultFlag.NEW));
assertThat(discoveryResult.getThingUID(), is(new ThingUID("tradfri:0107:1:65538")));
assertThat(discoveryResult.getThingTypeUID(), is(THING_TYPE_MOTION_SENSOR));
assertThat(discoveryResult.getBridgeUID(), is(GATEWAY_THING_UID));
assertThat(discoveryResult.getProperties().get(CONFIG_ID), is(65538));
assertThat(discoveryResult.getRepresentationProperty(), is(CONFIG_ID));
}
}

View File

@@ -0,0 +1,83 @@
/**
* 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.tradfri.internal.model;
import static org.junit.Assert.*;
import java.util.Arrays;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.Test;
/**
* Tests for {@link TradfriVersion}.
*
* @author Christoph Weitkamp - Initial contribution
*/
@NonNullByDefault
public class TradfriVersionTest {
private static final int LESS_THAN = -1;
private static final int EQUAL_TO = 0;
private static final int GREATER_THAN = 1;
private static final String VERSION_STRING = "1.2.42";
private static final TradfriVersion VERSION = new TradfriVersion(VERSION_STRING);
@Test(expected = IllegalArgumentException.class)
public void testIllegalArgumentException() throws IllegalArgumentException {
new TradfriVersion("FAILURE");
}
@Test
public void testParts() {
assertEquals(Arrays.asList(1, 2, 42), VERSION.parts);
}
@Test
public void testCompareToEqualTo() {
assertEquals(EQUAL_TO, VERSION.compareTo(VERSION));
assertEquals(EQUAL_TO, VERSION.compareTo(new TradfriVersion(VERSION_STRING)));
}
@Test
public void testCompareToLessThan() {
assertEquals(LESS_THAN, VERSION.compareTo(new TradfriVersion("2")));
assertEquals(LESS_THAN, VERSION.compareTo(new TradfriVersion("1.3")));
assertEquals(LESS_THAN, VERSION.compareTo(new TradfriVersion("1.2.50")));
assertEquals(LESS_THAN, VERSION.compareTo(new TradfriVersion("1.2.42.5")));
}
@Test
public void testCompareToGreaterThan() {
assertEquals(GREATER_THAN, VERSION.compareTo(new TradfriVersion("1")));
assertEquals(GREATER_THAN, VERSION.compareTo(new TradfriVersion("1.1")));
assertEquals(GREATER_THAN, VERSION.compareTo(new TradfriVersion("1.2.30")));
}
@SuppressWarnings("unlikely-arg-type")
@Test
public void testEquals() {
assertTrue(VERSION.equals(VERSION));
assertTrue(VERSION.equals(new TradfriVersion(VERSION_STRING)));
assertFalse(VERSION.equals((TradfriVersion) null));
assertFalse(VERSION.equals(new Integer("1")));
assertFalse(VERSION.equals(new TradfriVersion("1.2.5")));
}
@Test
public void testToString() {
assertEquals(VERSION_STRING, VERSION.toString());
}
}