[deconz] add group support (#8715)

* add group message

Signed-off-by: Jan N. Klug <jan.n.klug@rub.de>
This commit is contained in:
J-N-K 2020-10-31 17:17:06 +01:00 committed by GitHub
parent 1ac55a58e0
commit 8abcc252df
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 860 additions and 259 deletions

View File

@ -44,7 +44,7 @@
/bundles/org.openhab.binding.daikin/ @caffineehacker /bundles/org.openhab.binding.daikin/ @caffineehacker
/bundles/org.openhab.binding.danfossairunit/ @pravussum /bundles/org.openhab.binding.danfossairunit/ @pravussum
/bundles/org.openhab.binding.darksky/ @cweitkamp /bundles/org.openhab.binding.darksky/ @cweitkamp
/bundles/org.openhab.binding.deconz/ @davidgraeff /bundles/org.openhab.binding.deconz/ @J-N-K
/bundles/org.openhab.binding.denonmarantz/ @jwveldhuis /bundles/org.openhab.binding.denonmarantz/ @jwveldhuis
/bundles/org.openhab.binding.digiplex/ @rmichalak /bundles/org.openhab.binding.digiplex/ @rmichalak
/bundles/org.openhab.binding.digitalstrom/ @MichaelOchel @msiegele /bundles/org.openhab.binding.digitalstrom/ @MichaelOchel @msiegele

View File

@ -42,10 +42,12 @@ Additionally lights, window coverings (blinds) and thermostats are supported:
| Thermostat | ZHAThermostat | `thermostat` | | Thermostat | ZHAThermostat | `thermostat` |
| Warning Device (Siren) | Warning device | `warningdevice` | | Warning Device (Siren) | Warning device | `warningdevice` |
Currently only light-groups are supported via the thing-type `lightgroup`.
## Discovery ## Discovery
deCONZ software instances are discovered automatically in the same subnet. deCONZ software instances are discovered automatically in the same subnet.
Sensors, switches, lights and blinds are discovered as soon as a `deconz` bridge thing comes online. Sensors, switches, groups, lights and blinds are discovered as soon as a `deconz` bridge thing comes online.
If your device is not discovered, please check the DEBUG log for unknown devices and report your findings. If your device is not discovered, please check the DEBUG log for unknown devices and report your findings.
## Thing Configuration ## Thing Configuration
@ -81,13 +83,11 @@ Due to limitations in the API of deCONZ, the `lastSeen` channel (available some
Allowed values are all positive integers, the unit is minutes. Allowed values are all positive integers, the unit is minutes.
The default-value is `0`, which means "no polling at all". The default-value is `0`, which means "no polling at all".
`dimmablelight`, `extendedcolorlight`, `colorlight` and `colortemperaturelight` have an additional optional parameter `transitiontime`. `dimmablelight`, `extendedcolorlight`, `colorlight` and `colortemperaturelight` have an additional optional parameter `transitiontime`.
The transition time is the time to move between two states and is configured in seconds. The transition time is the time to move between two states and is configured in seconds.
The resolution provided is 1/10s. The resolution provided is 1/10s.
If no value is provided, the default value of the device is used. If no value is provided, the default value of the device is used.
### Textual Thing Configuration - Retrieving an API Key ### Textual Thing Configuration - Retrieving an API Key
If you use the textual configuration, the thing file without an API key will look like this, for example: If you use the textual configuration, the thing file without an API key will look like this, for example:
@ -154,18 +154,23 @@ The `last_seen` channel is added when it is available AND the `lastSeenPolling`
Other devices support Other devices support
| Channel Type ID | Item Type | Access Mode | Description | Thing types | | Channel Type ID | Item Type | Access Mode | Description | Thing types |
|-------------------|--------------------------|:-----------:|---------------------------------------|-----------------------------------------------| |-------------------|--------------------------|:-----------:|---------------------------------------|-------------------------------------------------|
| brightness | Dimmer | R/W | Brightness of the light | `dimmablelight`, `colortemperaturelight` | | brightness | Dimmer | R/W | Brightness of the light | `dimmablelight`, `colortemperaturelight` |
| switch | Switch | R/W | State of a ON/OFF device | `onofflight` | | switch | Switch | R/W | State of a ON/OFF device | `onofflight` |
| color | Color | R/W | Color of an multi-color light | `colorlight`, `extendedcolorlight` | | color | Color | R/W | Color of an multi-color light | `colorlight`, `extendedcolorlight`, `lightgroup`|
| color_temperature | Number | R/W | Color temperature in kelvin. The value range is determined by each individual light | `colortemperaturelight`, `extendedcolorlight` | | color_temperature | Number | R/W | Color temperature in kelvin. The value range is determined by each individual light | `colortemperaturelight`, `extendedcolorlight`, `lightgroup` |
| position | Rollershutter | R/W | Position of the blind | `windowcovering` | | position | Rollershutter | R/W | Position of the blind | `windowcovering` |
| heatsetpoint | Number:Temperature | R/W | Target Temperature in °C | `thermostat` | | heatsetpoint | Number:Temperature | R/W | Target Temperature in °C | `thermostat` |
| valve | Number:Dimensionless | R | Valve position in % | `thermostat` | | valve | Number:Dimensionless | R | Valve position in % | `thermostat` |
| mode | String | R/W | Mode: "auto", "heat" and "off" | `thermostat` | | mode | String | R/W | Mode: "auto", "heat" and "off" | `thermostat` |
| offset | Number | R | Temperature offset for sensor | `thermostat` | | offset | Number | R | Temperature offset for sensor | `thermostat` |
| alert | Switch | R/W | Turn alerts on/off | `warningdevice` | | alert | Switch | R/W | Turn alerts on/off | `warningdevice`, `lightgroup` |
| all_on | Switch | R | All lights in group are on | `lightgroup` |
| any_on | Switch | R | Any light in group is on | `lightgroup` |
**NOTE:** For groups `color` and `color_temperature` are used for sending commands to the group.
Their state represents the last command send to the group, not necessarily the actual state of the group.
### Trigger Channels ### Trigger Channels
@ -207,6 +212,7 @@ Bridge deconz:deconz:homeserver [ host="192.168.0.10", apikey="ABCDEFGHIJ" ] {
waterleakagesensor basement-water-leakage "Basement Water Leakage" [ id="7" ] waterleakagesensor basement-water-leakage "Basement Water Leakage" [ id="7" ]
alarmsensor basement-alarm "Basement Alarm Sensor" [ id="8", lastSeenPolling=5 ] alarmsensor basement-alarm "Basement Alarm Sensor" [ id="8", lastSeenPolling=5 ]
dimmablelight livingroom-ceiling "Livingroom Ceiling" [ id="1" ] dimmablelight livingroom-ceiling "Livingroom Ceiling" [ id="1" ]
lightgroup livingroom "Livingroom" [ id="1" ]
} }
``` ```
@ -221,6 +227,7 @@ Contact Livingroom_Window "Window Livingroom [%s]"
Switch Basement_Water_Leakage "Basement Water Leakage [%s]" { channel="deconz:waterleakagesensor:homeserver:basement-water-leakage:waterleakage" } Switch Basement_Water_Leakage "Basement Water Leakage [%s]" { channel="deconz:waterleakagesensor:homeserver:basement-water-leakage:waterleakage" }
Switch Basement_Alarm "Basement Alarm Triggered [%s]" { channel="deconz:alarmsensor:homeserver:basement-alarm:alarm" } Switch Basement_Alarm "Basement Alarm Triggered [%s]" { channel="deconz:alarmsensor:homeserver:basement-alarm:alarm" }
Dimmer Livingroom_Ceiling "Livingroom Ceiling [%d]" <light> { channel="deconz:dimmablelight:homeserver:livingroom-ceiling:brightness" } Dimmer Livingroom_Ceiling "Livingroom Ceiling [%d]" <light> { channel="deconz:dimmablelight:homeserver:livingroom-ceiling:brightness" }
Color Livingroom "Livingroom Light Control"
``` ```
### Events ### Events

View File

@ -23,7 +23,6 @@ import org.openhab.core.thing.ThingTypeUID;
*/ */
@NonNullByDefault @NonNullByDefault
public class BindingConstants { public class BindingConstants {
public static final String BINDING_ID = "deconz"; public static final String BINDING_ID = "deconz";
// List of all Thing Type UIDs // List of all Thing Type UIDs
@ -63,7 +62,10 @@ public class BindingConstants {
public static final ThingTypeUID THING_TYPE_WINDOW_COVERING = new ThingTypeUID(BINDING_ID, "windowcovering"); public static final ThingTypeUID THING_TYPE_WINDOW_COVERING = new ThingTypeUID(BINDING_ID, "windowcovering");
public static final ThingTypeUID THING_TYPE_WARNING_DEVICE = new ThingTypeUID(BINDING_ID, "warningdevice"); public static final ThingTypeUID THING_TYPE_WARNING_DEVICE = new ThingTypeUID(BINDING_ID, "warningdevice");
// List of all Channel ids // groups
public static final ThingTypeUID THING_TYPE_LIGHTGROUP = new ThingTypeUID(BINDING_ID, "lightgroup");
// sensor channel ids
public static final String CHANNEL_PRESENCE = "presence"; public static final String CHANNEL_PRESENCE = "presence";
public static final String CHANNEL_LAST_UPDATED = "last_updated"; public static final String CHANNEL_LAST_UPDATED = "last_updated";
public static final String CHANNEL_LAST_SEEN = "last_seen"; public static final String CHANNEL_LAST_SEEN = "last_seen";
@ -98,19 +100,22 @@ public class BindingConstants {
public static final String CHANNEL_TEMPERATURE_OFFSET = "offset"; public static final String CHANNEL_TEMPERATURE_OFFSET = "offset";
public static final String CHANNEL_VALVE_POSITION = "valve"; public static final String CHANNEL_VALVE_POSITION = "valve";
// group + light channel ids
public static final String CHANNEL_SWITCH = "switch"; public static final String CHANNEL_SWITCH = "switch";
public static final String CHANNEL_BRIGHTNESS = "brightness"; public static final String CHANNEL_BRIGHTNESS = "brightness";
public static final String CHANNEL_COLOR_TEMPERATURE = "color_temperature"; public static final String CHANNEL_COLOR_TEMPERATURE = "color_temperature";
public static final String CHANNEL_COLOR = "color"; public static final String CHANNEL_COLOR = "color";
public static final String CHANNEL_POSITION = "position"; public static final String CHANNEL_POSITION = "position";
public static final String CHANNEL_ALERT = "alert"; public static final String CHANNEL_ALERT = "alert";
public static final String CHANNEL_ALL_ON = "all_on";
public static final String CHANNEL_ANY_ON = "any_on";
// Thing configuration // Thing configuration
public static final String CONFIG_HOST = "host"; public static final String CONFIG_HOST = "host";
public static final String CONFIG_HTTP_PORT = "httpPort"; public static final String CONFIG_HTTP_PORT = "httpPort";
public static final String CONFIG_APIKEY = "apikey"; public static final String CONFIG_APIKEY = "apikey";
public static final String PROPERTY_UDN = "UDN"; public static final String PROPERTY_UDN = "UDN";
public static final String CONFIG_ID = "id";
public static final String UNIQUE_ID = "uid"; public static final String UNIQUE_ID = "uid";
public static final String PROPERTY_CT_MIN = "ctmin"; public static final String PROPERTY_CT_MIN = "ctmin";
@ -121,4 +126,7 @@ public class BindingConstants {
public static final int ZCL_CT_MIN = 1; public static final int ZCL_CT_MIN = 1;
public static final int ZCL_CT_MAX = 65279; // 0xFEFF public static final int ZCL_CT_MAX = 65279; // 0xFEFF
public static final int ZCL_CT_INVALID = 65535; // 0xFFFF public static final int ZCL_CT_INVALID = 65535; // 0xFFFF
public static final double HUE_FACTOR = 65535 / 360.0;
public static final double BRIGHTNESS_FACTOR = 2.54;
} }

View File

@ -18,15 +18,9 @@ import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.deconz.internal.handler.DeconzBridgeHandler; import org.openhab.binding.deconz.internal.handler.*;
import org.openhab.binding.deconz.internal.handler.LightThingHandler;
import org.openhab.binding.deconz.internal.handler.SensorThermostatThingHandler;
import org.openhab.binding.deconz.internal.handler.SensorThingHandler;
import org.openhab.binding.deconz.internal.netutils.AsyncHttpClient; import org.openhab.binding.deconz.internal.netutils.AsyncHttpClient;
import org.openhab.binding.deconz.internal.types.LightType; import org.openhab.binding.deconz.internal.types.*;
import org.openhab.binding.deconz.internal.types.LightTypeDeserializer;
import org.openhab.binding.deconz.internal.types.ThermostatMode;
import org.openhab.binding.deconz.internal.types.ThermostatModeGsonTypeAdapter;
import org.openhab.core.io.net.http.HttpClientFactory; import org.openhab.core.io.net.http.HttpClientFactory;
import org.openhab.core.io.net.http.WebSocketFactory; import org.openhab.core.io.net.http.WebSocketFactory;
import org.openhab.core.thing.Bridge; import org.openhab.core.thing.Bridge;
@ -53,7 +47,8 @@ import com.google.gson.GsonBuilder;
public class DeconzHandlerFactory extends BaseThingHandlerFactory { public class DeconzHandlerFactory extends BaseThingHandlerFactory {
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Stream private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Stream
.of(DeconzBridgeHandler.SUPPORTED_THING_TYPES, LightThingHandler.SUPPORTED_THING_TYPE_UIDS, .of(DeconzBridgeHandler.SUPPORTED_THING_TYPES, LightThingHandler.SUPPORTED_THING_TYPE_UIDS,
SensorThingHandler.SUPPORTED_THING_TYPES, SensorThermostatThingHandler.SUPPORTED_THING_TYPES) SensorThingHandler.SUPPORTED_THING_TYPES, SensorThermostatThingHandler.SUPPORTED_THING_TYPES,
GroupThingHandler.SUPPORTED_THING_TYPE_UIDS)
.flatMap(Set::stream).collect(Collectors.toSet()); .flatMap(Set::stream).collect(Collectors.toSet());
private final Gson gson; private final Gson gson;
@ -71,6 +66,8 @@ public class DeconzHandlerFactory extends BaseThingHandlerFactory {
GsonBuilder gsonBuilder = new GsonBuilder(); GsonBuilder gsonBuilder = new GsonBuilder();
gsonBuilder.registerTypeAdapter(LightType.class, new LightTypeDeserializer()); gsonBuilder.registerTypeAdapter(LightType.class, new LightTypeDeserializer());
gsonBuilder.registerTypeAdapter(GroupType.class, new GroupTypeDeserializer());
gsonBuilder.registerTypeAdapter(ResourceType.class, new ResourceTypeDeserializer());
gsonBuilder.registerTypeAdapter(ThermostatMode.class, new ThermostatModeGsonTypeAdapter()); gsonBuilder.registerTypeAdapter(ThermostatMode.class, new ThermostatModeGsonTypeAdapter());
gson = gsonBuilder.create(); gson = gsonBuilder.create();
} }
@ -93,6 +90,8 @@ public class DeconzHandlerFactory extends BaseThingHandlerFactory {
return new SensorThingHandler(thing, gson); return new SensorThingHandler(thing, gson);
} else if (SensorThermostatThingHandler.SUPPORTED_THING_TYPES.contains(thingTypeUID)) { } else if (SensorThermostatThingHandler.SUPPORTED_THING_TYPES.contains(thingTypeUID)) {
return new SensorThermostatThingHandler(thing, gson); return new SensorThermostatThingHandler(thing, gson);
} else if (GroupThingHandler.SUPPORTED_THING_TYPE_UIDS.contains(thingTypeUID)) {
return new GroupThingHandler(thing, gson);
} }
return null; return null;

View File

@ -12,6 +12,8 @@
*/ */
package org.openhab.binding.deconz.internal; package org.openhab.binding.deconz.internal;
import static org.openhab.binding.deconz.internal.BindingConstants.BRIGHTNESS_FACTOR;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.ZoneId; import java.time.ZoneId;
import java.time.ZoneOffset; import java.time.ZoneOffset;
@ -22,6 +24,9 @@ import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.library.types.DateTimeType; import org.openhab.core.library.types.DateTimeType;
import org.openhab.core.library.types.PercentType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/** /**
* The {@link Util} class defines common utility methods * The {@link Util} class defines common utility methods
@ -30,6 +35,8 @@ import org.openhab.core.library.types.DateTimeType;
*/ */
@NonNullByDefault @NonNullByDefault
public class Util { public class Util {
private static final Logger LOGGER = LoggerFactory.getLogger(Util.class);
public static String buildUrl(String host, int port, String... urlParts) { public static String buildUrl(String host, int port, String... urlParts) {
StringBuilder url = new StringBuilder(); StringBuilder url = new StringBuilder();
url.append("http://"); url.append("http://");
@ -54,6 +61,33 @@ public class Util {
return Math.max(min, Math.min(intValue, max)); return Math.max(min, Math.min(intValue, max));
} }
/**
* convert a brightness value from int to PercentType
*
* @param val the value
* @return the corresponding PercentType value
*/
public static PercentType toPercentType(int val) {
int scaledValue = (int) Math.ceil(val / BRIGHTNESS_FACTOR);
if (scaledValue < 0 || scaledValue > 100) {
LOGGER.trace("received value {} (converted to {}). Coercing.", val, scaledValue);
scaledValue = scaledValue < 0 ? 0 : scaledValue;
scaledValue = scaledValue > 100 ? 100 : scaledValue;
}
return new PercentType(scaledValue);
}
/**
* convert a brightness value from PercentType to int
*
* @param val the value
* @return the corresponding int value
*/
public static int fromPercentType(PercentType val) {
return (int) Math.floor(val.doubleValue() * BRIGHTNESS_FACTOR);
}
/** /**
* convert a timestamp string to a DateTimeType * convert a timestamp string to a DateTimeType
* *

View File

@ -26,12 +26,14 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.deconz.internal.Util; import org.openhab.binding.deconz.internal.Util;
import org.openhab.binding.deconz.internal.dto.BridgeFullState; import org.openhab.binding.deconz.internal.dto.BridgeFullState;
import org.openhab.binding.deconz.internal.dto.GroupMessage;
import org.openhab.binding.deconz.internal.dto.LightMessage; import org.openhab.binding.deconz.internal.dto.LightMessage;
import org.openhab.binding.deconz.internal.dto.SensorMessage; import org.openhab.binding.deconz.internal.dto.SensorMessage;
import org.openhab.binding.deconz.internal.handler.DeconzBridgeHandler; import org.openhab.binding.deconz.internal.handler.DeconzBridgeHandler;
import org.openhab.binding.deconz.internal.handler.LightThingHandler; import org.openhab.binding.deconz.internal.handler.LightThingHandler;
import org.openhab.binding.deconz.internal.handler.SensorThermostatThingHandler; import org.openhab.binding.deconz.internal.handler.SensorThermostatThingHandler;
import org.openhab.binding.deconz.internal.handler.SensorThingHandler; import org.openhab.binding.deconz.internal.handler.SensorThingHandler;
import org.openhab.binding.deconz.internal.types.GroupType;
import org.openhab.binding.deconz.internal.types.LightType; import org.openhab.binding.deconz.internal.types.LightType;
import org.openhab.core.config.discovery.AbstractDiscoveryService; import org.openhab.core.config.discovery.AbstractDiscoveryService;
import org.openhab.core.config.discovery.DiscoveryResult; import org.openhab.core.config.discovery.DiscoveryResult;
@ -93,12 +95,53 @@ public class ThingDiscoveryService extends AbstractDiscoveryService implements D
} }
/** /**
* Add a sensor device to the discovery inbox. * Add a group to the discovery inbox.
* *
* @param lightID The id of the light * @param groupId The id of the light
* @param light The sensor description * @param group The group description
*/ */
private void addLight(String lightID, LightMessage light) { private void addGroup(String groupId, GroupMessage group) {
final ThingUID bridgeUID = this.bridgeUID;
if (bridgeUID == null) {
logger.warn("Received a message from non-existent bridge. This most likely is a bug.");
return;
}
ThingTypeUID thingTypeUID;
GroupType groupType = group.type;
if (groupType == null) {
logger.warn("No group type reported for group {} ({})", group.modelid, group.name);
return;
}
Map<String, Object> properties = new HashMap<>();
properties.put(CONFIG_ID, groupId);
switch (groupType) {
case LIGHT_GROUP:
thingTypeUID = THING_TYPE_LIGHTGROUP;
break;
default:
logger.debug(
"Found group: {} ({}), type {} but no thing type defined for that type. This should be reported.",
group.id, group.name, group.type);
return;
}
ThingUID uid = new ThingUID(thingTypeUID, bridgeUID, group.id);
DiscoveryResult discoveryResult = DiscoveryResultBuilder.create(uid).withBridge(bridgeUID).withLabel(group.name)
.withProperties(properties).withRepresentationProperty(CONFIG_ID).build();
thingDiscovered(discoveryResult);
}
/**
* Add a light device to the discovery inbox.
*
* @param lightId The id of the light
* @param light The light description
*/
private void addLight(String lightId, LightMessage light) {
final ThingUID bridgeUID = this.bridgeUID; final ThingUID bridgeUID = this.bridgeUID;
if (bridgeUID == null) { if (bridgeUID == null) {
logger.warn("Received a message from non-existent bridge. This most likely is a bug."); logger.warn("Received a message from non-existent bridge. This most likely is a bug.");
@ -114,7 +157,7 @@ public class ThingDiscoveryService extends AbstractDiscoveryService implements D
} }
Map<String, Object> properties = new HashMap<>(); Map<String, Object> properties = new HashMap<>();
properties.put("id", lightID); properties.put(CONFIG_ID, lightId);
properties.put(UNIQUE_ID, light.uniqueid); properties.put(UNIQUE_ID, light.uniqueid);
properties.put(Thing.PROPERTY_FIRMWARE_VERSION, light.swversion); properties.put(Thing.PROPERTY_FIRMWARE_VERSION, light.swversion);
properties.put(Thing.PROPERTY_VENDOR, light.manufacturername); properties.put(Thing.PROPERTY_VENDOR, light.manufacturername);
@ -227,7 +270,7 @@ public class ThingDiscoveryService extends AbstractDiscoveryService implements D
ThingUID uid = new ThingUID(thingTypeUID, bridgeUID, sensor.uniqueid.replaceAll("[^a-z0-9\\[\\]]", "")); ThingUID uid = new ThingUID(thingTypeUID, bridgeUID, sensor.uniqueid.replaceAll("[^a-z0-9\\[\\]]", ""));
DiscoveryResult discoveryResult = DiscoveryResultBuilder.create(uid).withBridge(bridgeUID) DiscoveryResult discoveryResult = DiscoveryResultBuilder.create(uid).withBridge(bridgeUID)
.withLabel(sensor.name + " (" + sensor.manufacturername + ")").withProperty("id", sensorID) .withLabel(sensor.name + " (" + sensor.manufacturername + ")").withProperty(CONFIG_ID, sensorID)
.withProperty(UNIQUE_ID, sensor.uniqueid).withRepresentationProperty(UNIQUE_ID).build(); .withProperty(UNIQUE_ID, sensor.uniqueid).withRepresentationProperty(UNIQUE_ID).build();
thingDiscovered(discoveryResult); thingDiscovered(discoveryResult);
} }
@ -268,6 +311,7 @@ public class ThingDiscoveryService extends AbstractDiscoveryService implements D
if (fullState != null) { if (fullState != null) {
fullState.sensors.forEach(this::addSensor); fullState.sensors.forEach(this::addSensor);
fullState.lights.forEach(this::addLight); fullState.lights.forEach(this::addLight);
fullState.groups.forEach(this::addGroup);
} }
} }
} }

View File

@ -41,4 +41,5 @@ public class BridgeFullState {
public Map<String, SensorMessage> sensors = Collections.emptyMap(); public Map<String, SensorMessage> sensors = Collections.emptyMap();
public Map<String, LightMessage> lights = Collections.emptyMap(); public Map<String, LightMessage> lights = Collections.emptyMap();
public Map<String, GroupMessage> groups = Collections.emptyMap();
} }

View File

@ -14,6 +14,7 @@ package org.openhab.binding.deconz.internal.dto;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.deconz.internal.types.ResourceType;
/** /**
* The REST interface and websocket connection are using the same fields. * The REST interface and websocket connection are using the same fields.
@ -25,7 +26,7 @@ import org.eclipse.jdt.annotation.Nullable;
public class DeconzBaseMessage { public class DeconzBaseMessage {
// For websocket change events // For websocket change events
public String e = ""; // "changed" public String e = ""; // "changed"
public String r = ""; // "sensors" public ResourceType r = ResourceType.UNKNOWN; // "sensors"
public String t = ""; // "event" public String t = ""; // "event"
public String id = ""; // "3" public String id = ""; // "3"

View File

@ -0,0 +1,46 @@
/**
* 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.deconz.internal.dto;
import java.util.Arrays;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* The {@link GroupAction} is send by the websocket connection as well as the Rest API.
* It is part of a {@link GroupMessage}.
*
* @author Jan N. Klug - Initial contribution
*/
@NonNullByDefault
public class GroupAction {
public @Nullable Boolean on;
public @Nullable Boolean toggle;
public @Nullable Integer bri;
public @Nullable Integer hue;
public @Nullable Integer sat;
public @Nullable Integer ct;
public double @Nullable [] xy;
public @Nullable String alert;
public @Nullable String effect;
public @Nullable Integer colorloopspeed;
public @Nullable Integer transitiontime;
@Override
public String toString() {
return "GroupAction{" + "on=" + on + ", toggle=" + toggle + ", bri=" + bri + ", hue=" + hue + ", sat=" + sat
+ ", ct=" + ct + ", xy=" + Arrays.toString(xy) + ", alert='" + alert + '\'' + ", effect='" + effect
+ '\'' + ", colorloopspeed=" + colorloopspeed + ", transitiontime=" + transitiontime + '}';
}
}

View File

@ -0,0 +1,46 @@
/**
* 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.deconz.internal.dto;
import java.util.Arrays;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.deconz.internal.types.GroupType;
/**
* The REST interface and websocket connection are using the same fields.
* The REST data contains more descriptive info like the manufacturer and name.
*
* @author Jan N. Klug - Initial contribution
*/
@NonNullByDefault
public class GroupMessage extends DeconzBaseMessage {
public @Nullable GroupAction action;
public String @Nullable [] devicemembership;
public @Nullable Boolean hidden;
public String @Nullable [] lights;
public String @Nullable [] lightsequence;
public String @Nullable [] multideviceids;
public Scene @Nullable [] scenes;
public @Nullable GroupState state;
public @Nullable GroupType type;
@Override
public String toString() {
return "GroupMessage{" + "action=" + action + ", devicemembership=" + Arrays.toString(devicemembership)
+ ", hidden=" + hidden + ", lights=" + Arrays.toString(lights) + ", lightsequence="
+ Arrays.toString(lightsequence) + ", multideviceids=" + Arrays.toString(multideviceids) + ", scenes="
+ Arrays.toString(scenes) + ", state=" + state + ", type=" + type + '}';
}
}

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.deconz.internal.dto;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link GroupState} is send by the websocket connection as well as the Rest API.
* It is part of a {@link GroupMessage}.
*
* @author Jan N. Klug - Initial contribution
*/
@NonNullByDefault
public class GroupState {
public boolean all_on;
public boolean any_on;
@Override
public String toString() {
return "GroupState{" + "all_on=" + all_on + ", any_on=" + any_on + '}';
}
}

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.deconz.internal.dto;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link Scene} is send by the websocket connection as well as the Rest API.
* It is part of a {@link GroupMessage}.
*
* @author Jan N. Klug - Initial contribution
*/
@NonNullByDefault
public class Scene {
public String id = "";
public String name = "";
}

View File

@ -26,12 +26,10 @@ import org.openhab.binding.deconz.internal.dto.DeconzBaseMessage;
import org.openhab.binding.deconz.internal.netutils.AsyncHttpClient; import org.openhab.binding.deconz.internal.netutils.AsyncHttpClient;
import org.openhab.binding.deconz.internal.netutils.WebSocketConnection; import org.openhab.binding.deconz.internal.netutils.WebSocketConnection;
import org.openhab.binding.deconz.internal.netutils.WebSocketMessageListener; import org.openhab.binding.deconz.internal.netutils.WebSocketMessageListener;
import org.openhab.core.thing.Bridge; import org.openhab.binding.deconz.internal.types.ResourceType;
import org.openhab.core.thing.Thing; import org.openhab.core.thing.*;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.ThingStatusInfo;
import org.openhab.core.thing.binding.BaseThingHandler; import org.openhab.core.thing.binding.BaseThingHandler;
import org.openhab.core.types.Command;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -42,9 +40,7 @@ import com.google.gson.Gson;
* *
* It waits for the bridge to come online, grab the websocket connection and bridge configuration * It waits for the bridge to come online, grab the websocket connection and bridge configuration
* and registers to the websocket connection as a listener. * and registers to the websocket connection as a listener.
* **
* A REST API call is made to get the initial light/rollershutter state.
*
* @author David Graeff - Initial contribution * @author David Graeff - Initial contribution
* @author Jan N. Klug - Refactored to abstract class * @author Jan N. Klug - Refactored to abstract class
*/ */
@ -52,6 +48,7 @@ import com.google.gson.Gson;
public abstract class DeconzBaseThingHandler<T extends DeconzBaseMessage> extends BaseThingHandler public abstract class DeconzBaseThingHandler<T extends DeconzBaseMessage> extends BaseThingHandler
implements WebSocketMessageListener { implements WebSocketMessageListener {
private final Logger logger = LoggerFactory.getLogger(DeconzBaseThingHandler.class); private final Logger logger = LoggerFactory.getLogger(DeconzBaseThingHandler.class);
protected final ResourceType resourceType;
protected ThingConfig config = new ThingConfig(); protected ThingConfig config = new ThingConfig();
protected DeconzBridgeConfig bridgeConfig = new DeconzBridgeConfig(); protected DeconzBridgeConfig bridgeConfig = new DeconzBridgeConfig();
protected final Gson gson; protected final Gson gson;
@ -59,9 +56,10 @@ public abstract class DeconzBaseThingHandler<T extends DeconzBaseMessage> extend
protected @Nullable WebSocketConnection connection; protected @Nullable WebSocketConnection connection;
protected @Nullable AsyncHttpClient http; protected @Nullable AsyncHttpClient http;
public DeconzBaseThingHandler(Thing thing, Gson gson) { public DeconzBaseThingHandler(Thing thing, Gson gson, ResourceType resourceType) {
super(thing); super(thing);
this.gson = gson; this.gson = gson;
this.resourceType = resourceType;
} }
/** /**
@ -75,9 +73,19 @@ public abstract class DeconzBaseThingHandler<T extends DeconzBaseMessage> extend
} }
} }
protected abstract void registerListener(); private void registerListener() {
WebSocketConnection conn = connection;
if (conn != null) {
conn.registerListener(resourceType, config.id, this);
}
}
protected abstract void unregisterListener(); private void unregisterListener() {
WebSocketConnection conn = connection;
if (conn != null) {
conn.unregisterListener(resourceType, config.id);
}
}
@Override @Override
public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) { public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
@ -86,39 +94,38 @@ public abstract class DeconzBaseThingHandler<T extends DeconzBaseMessage> extend
return; return;
} }
if (bridgeStatusInfo.getStatus() == ThingStatus.OFFLINE) { if (bridgeStatusInfo.getStatus() == ThingStatus.ONLINE) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE); // the bridge is ONLINE, we can communicate with the gateway, so we update the connection parameters and
// register the listener
Bridge bridge = getBridge();
if (bridge == null) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
return;
}
DeconzBridgeHandler bridgeHandler = (DeconzBridgeHandler) bridge.getHandler();
if (bridgeHandler == null) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
return;
}
final WebSocketConnection webSocketConnection = bridgeHandler.getWebsocketConnection();
this.connection = webSocketConnection;
this.http = bridgeHandler.getHttp();
this.bridgeConfig = bridgeHandler.getBridgeConfig();
updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.NONE);
// Real-time data
registerListener();
// get initial values
requestState();
} else {
// if the bridge is not ONLINE, we assume communication is not possible, so we unregister the listener and
// set the thing status to OFFLINE
unregisterListener(); unregisterListener();
return;
}
if (bridgeStatusInfo.getStatus() != ThingStatus.ONLINE) {
return;
}
Bridge bridge = getBridge();
if (bridge == null) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE); updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
return;
} }
DeconzBridgeHandler bridgeHandler = (DeconzBridgeHandler) bridge.getHandler();
if (bridgeHandler == null) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
return;
}
final WebSocketConnection webSocketConnection = bridgeHandler.getWebsocketConnection();
this.connection = webSocketConnection;
this.http = bridgeHandler.getHttp();
this.bridgeConfig = bridgeHandler.getBridgeConfig();
updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.NONE);
// Real-time data
registerListener();
// get initial values
requestState();
} }
protected abstract @Nullable T parseStateResponse(AsyncHttpClient.Result r); protected abstract @Nullable T parseStateResponse(AsyncHttpClient.Result r);
@ -132,21 +139,17 @@ public abstract class DeconzBaseThingHandler<T extends DeconzBaseMessage> extend
*/ */
protected abstract void processStateResponse(@Nullable T stateResponse); protected abstract void processStateResponse(@Nullable T stateResponse);
/**
* call requestState(type) in this method only
*/
protected abstract void requestState();
/** /**
* Perform a request to the REST API for retrieving the full light state with all data and configuration. * Perform a request to the REST API for retrieving the full light state with all data and configuration.
*/ */
protected void requestState(String type) { protected void requestState() {
AsyncHttpClient asyncHttpClient = http; AsyncHttpClient asyncHttpClient = http;
if (asyncHttpClient == null) { if (asyncHttpClient == null) {
return; return;
} }
String url = buildUrl(bridgeConfig.host, bridgeConfig.httpPort, bridgeConfig.apikey, type, config.id); String url = buildUrl(bridgeConfig.host, bridgeConfig.httpPort, bridgeConfig.apikey,
resourceType.getIdentifier(), config.id);
logger.trace("Requesting URL for initial data: {}", url); logger.trace("Requesting URL for initial data: {}", url);
// Get initial data // Get initial data
@ -165,13 +168,42 @@ public abstract class DeconzBaseThingHandler<T extends DeconzBaseMessage> extend
}).thenAccept(this::processStateResponse); }).thenAccept(this::processStateResponse);
} }
/**
* sends a command to the bridge
*
* @param object must be serializable and contain the command
* @param originalCommand the original openHAB command (used for logging purposes)
* @param channelUID the channel that this command was send to (used for logging purposes)
* @param acceptProcessing additional processing after the command was successfully send (might be null)
*/
protected void sendCommand(Object object, Command originalCommand, ChannelUID channelUID,
@Nullable Runnable acceptProcessing) {
AsyncHttpClient asyncHttpClient = http;
if (asyncHttpClient == null) {
return;
}
String url = buildUrl(bridgeConfig.host, bridgeConfig.httpPort, bridgeConfig.apikey,
resourceType.getIdentifier(), config.id, resourceType.getCommandUrl());
String json = gson.toJson(object);
logger.trace("Sending {} to {} {} via {}", json, resourceType, config.id, url);
asyncHttpClient.put(url, json, bridgeConfig.timeout).thenAccept(v -> {
if (acceptProcessing != null) {
acceptProcessing.run();
}
logger.trace("Result code={}, body={}", v.getResponseCode(), v.getBody());
}).exceptionally(e -> {
logger.debug("Sending command {} to channel {} failed: {} - {}", originalCommand, channelUID, e.getClass(),
e.getMessage());
return null;
});
}
@Override @Override
public void dispose() { public void dispose() {
stopInitializationJob(); stopInitializationJob();
WebSocketConnection webSocketConnection = connection; unregisterListener();
if (webSocketConnection != null) {
webSocketConnection.unregisterLightListener(config.id);
}
super.dispose(); super.dispose();
} }

View File

@ -0,0 +1,172 @@
/**
* 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.deconz.internal.handler;
import static org.openhab.binding.deconz.internal.BindingConstants.*;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.deconz.internal.Util;
import org.openhab.binding.deconz.internal.dto.DeconzBaseMessage;
import org.openhab.binding.deconz.internal.dto.GroupAction;
import org.openhab.binding.deconz.internal.dto.GroupMessage;
import org.openhab.binding.deconz.internal.dto.GroupState;
import org.openhab.binding.deconz.internal.netutils.AsyncHttpClient;
import org.openhab.binding.deconz.internal.types.ResourceType;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.HSBType;
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.thing.ThingTypeUID;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
/**
* This light thing doesn't establish any connections, that is done by the bridge Thing.
*
* It waits for the bridge to come online, grab the websocket connection and bridge configuration
* and registers to the websocket connection as a listener.
*
* A REST API call is made to get the initial light/rollershutter state.
*
* Every light and rollershutter is supported by this Thing, because a unified state is kept
* in {@link #groupStateCache}. Every field that got received by the REST API for this specific
* sensor is published to the framework.
*
* @author Jan N. Klug - Initial contribution
*/
@NonNullByDefault
public class GroupThingHandler extends DeconzBaseThingHandler<GroupMessage> {
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPE_UIDS = Set.of(THING_TYPE_LIGHTGROUP);
private final Logger logger = LoggerFactory.getLogger(GroupThingHandler.class);
/**
* The group state.
*/
private GroupState groupStateCache = new GroupState();
public GroupThingHandler(Thing thing, Gson gson) {
super(thing, gson, ResourceType.GROUPS);
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
String channelId = channelUID.getId();
GroupAction newGroupAction = new GroupAction();
switch (channelId) {
case CHANNEL_ALL_ON:
case CHANNEL_ANY_ON:
if (command instanceof RefreshType) {
valueUpdated(channelUID.getId(), groupStateCache);
return;
}
break;
case CHANNEL_ALERT:
if (command instanceof OnOffType) {
newGroupAction.alert = command == OnOffType.ON ? "alert" : "none";
} else {
return;
}
break;
case CHANNEL_COLOR:
if (command instanceof HSBType) {
HSBType hsbCommand = (HSBType) command;
newGroupAction.bri = Util.fromPercentType(hsbCommand.getBrightness());
if (newGroupAction.bri > 0) {
newGroupAction.hue = (int) (hsbCommand.getHue().doubleValue() * HUE_FACTOR);
newGroupAction.sat = Util.fromPercentType(hsbCommand.getSaturation());
}
} else if (command instanceof PercentType) {
newGroupAction.bri = Util.fromPercentType((PercentType) command);
} else if (command instanceof DecimalType) {
newGroupAction.bri = ((DecimalType) command).intValue();
} else if (command instanceof OnOffType) {
newGroupAction.on = OnOffType.ON.equals(command);
} else {
return;
}
break;
case CHANNEL_COLOR_TEMPERATURE:
if (command instanceof DecimalType) {
int miredValue = Util.kelvinToMired(((DecimalType) command).intValue());
newGroupAction.ct = Util.constrainToRange(miredValue, ZCL_CT_MIN, ZCL_CT_MAX);
} else {
return;
}
break;
default:
return;
}
if (newGroupAction.bri != null && newGroupAction.bri > 0) {
newGroupAction.on = true;
}
sendCommand(newGroupAction, command, channelUID, null);
}
@Override
protected @Nullable GroupMessage parseStateResponse(AsyncHttpClient.Result r) {
if (r.getResponseCode() == 403) {
return null;
} else if (r.getResponseCode() == 200) {
return gson.fromJson(r.getBody(), GroupMessage.class);
} else {
throw new IllegalStateException("Unknown status code " + r.getResponseCode() + " for full state request");
}
}
@Override
protected void processStateResponse(@Nullable GroupMessage stateResponse) {
if (stateResponse == null) {
return;
}
messageReceived(config.id, stateResponse);
}
private void valueUpdated(String channelId, GroupState newState) {
switch (channelId) {
case CHANNEL_ALL_ON:
updateState(channelId, OnOffType.from(newState.all_on));
break;
case CHANNEL_ANY_ON:
updateState(channelId, OnOffType.from(newState.any_on));
break;
default:
}
}
@Override
public void messageReceived(String sensorID, DeconzBaseMessage message) {
if (message instanceof GroupMessage) {
GroupMessage groupMessage = (GroupMessage) message;
logger.trace("{} received {}", thing.getUID(), groupMessage);
GroupState groupState = groupMessage.state;
if (groupState != null) {
updateStatus(ThingStatus.ONLINE);
thing.getChannels().stream().map(c -> c.getUID().getId()).forEach(c -> valueUpdated(c, groupState));
groupStateCache = groupState;
}
}
}
}

View File

@ -19,8 +19,6 @@ import java.math.BigDecimal;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.Set; 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.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jdt.annotation.Nullable;
@ -30,7 +28,7 @@ import org.openhab.binding.deconz.internal.dto.DeconzBaseMessage;
import org.openhab.binding.deconz.internal.dto.LightMessage; import org.openhab.binding.deconz.internal.dto.LightMessage;
import org.openhab.binding.deconz.internal.dto.LightState; import org.openhab.binding.deconz.internal.dto.LightState;
import org.openhab.binding.deconz.internal.netutils.AsyncHttpClient; import org.openhab.binding.deconz.internal.netutils.AsyncHttpClient;
import org.openhab.binding.deconz.internal.netutils.WebSocketConnection; import org.openhab.binding.deconz.internal.types.ResourceType;
import org.openhab.core.library.types.DecimalType; import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.HSBType; import org.openhab.core.library.types.HSBType;
import org.openhab.core.library.types.OnOffType; import org.openhab.core.library.types.OnOffType;
@ -68,12 +66,10 @@ import com.google.gson.Gson;
*/ */
@NonNullByDefault @NonNullByDefault
public class LightThingHandler extends DeconzBaseThingHandler<LightMessage> { public class LightThingHandler extends DeconzBaseThingHandler<LightMessage> {
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPE_UIDS = Stream.of(THING_TYPE_COLOR_TEMPERATURE_LIGHT, public static final Set<ThingTypeUID> SUPPORTED_THING_TYPE_UIDS = Set.of(THING_TYPE_COLOR_TEMPERATURE_LIGHT,
THING_TYPE_DIMMABLE_LIGHT, THING_TYPE_COLOR_LIGHT, THING_TYPE_EXTENDED_COLOR_LIGHT, THING_TYPE_ONOFF_LIGHT, THING_TYPE_DIMMABLE_LIGHT, THING_TYPE_COLOR_LIGHT, THING_TYPE_EXTENDED_COLOR_LIGHT, THING_TYPE_ONOFF_LIGHT,
THING_TYPE_WINDOW_COVERING, THING_TYPE_WARNING_DEVICE).collect(Collectors.toSet()); THING_TYPE_WINDOW_COVERING, THING_TYPE_WARNING_DEVICE);
private static final double HUE_FACTOR = 65535 / 360.0;
private static final double BRIGHTNESS_FACTOR = 2.54;
private static final long DEFAULT_COMMAND_EXPIRY_TIME = 250; // in ms private static final long DEFAULT_COMMAND_EXPIRY_TIME = 250; // in ms
private final Logger logger = LoggerFactory.getLogger(LightThingHandler.class); private final Logger logger = LoggerFactory.getLogger(LightThingHandler.class);
@ -94,8 +90,7 @@ public class LightThingHandler extends DeconzBaseThingHandler<LightMessage> {
private int ctMin = ZCL_CT_MIN; private int ctMin = ZCL_CT_MIN;
public LightThingHandler(Thing thing, Gson gson, StateDescriptionProvider stateDescriptionProvider) { public LightThingHandler(Thing thing, Gson gson, StateDescriptionProvider stateDescriptionProvider) {
super(thing, gson); super(thing, gson, ResourceType.LIGHTS);
this.stateDescriptionProvider = stateDescriptionProvider; this.stateDescriptionProvider = stateDescriptionProvider;
} }
@ -127,27 +122,6 @@ public class LightThingHandler extends DeconzBaseThingHandler<LightMessage> {
super.initialize(); super.initialize();
} }
@Override
protected void registerListener() {
WebSocketConnection conn = connection;
if (conn != null) {
conn.registerLightListener(config.id, this);
}
}
@Override
protected void unregisterListener() {
WebSocketConnection conn = connection;
if (conn != null) {
conn.unregisterLightListener(config.id);
}
}
@Override
protected void requestState() {
requestState("lights");
}
@Override @Override
public void handleCommand(ChannelUID channelUID, Command command) { public void handleCommand(ChannelUID channelUID, Command command) {
if (command instanceof RefreshType) { if (command instanceof RefreshType) {
@ -186,15 +160,15 @@ public class LightThingHandler extends DeconzBaseThingHandler<LightMessage> {
logger.warn("Failed to convert {} to xy-values", command); logger.warn("Failed to convert {} to xy-values", command);
} }
newLightState.xy = new double[] { xy[0].doubleValue() / 100.0, xy[1].doubleValue() / 100.0 }; newLightState.xy = new double[] { xy[0].doubleValue() / 100.0, xy[1].doubleValue() / 100.0 };
newLightState.bri = fromPercentType(hsbCommand.getBrightness()); newLightState.bri = Util.fromPercentType(hsbCommand.getBrightness());
} else { } else {
// default is colormode "hs" (used when colormode "hs" is set or colormode is unknown) // default is colormode "hs" (used when colormode "hs" is set or colormode is unknown)
newLightState.bri = fromPercentType(hsbCommand.getBrightness()); newLightState.bri = Util.fromPercentType(hsbCommand.getBrightness());
newLightState.hue = (int) (hsbCommand.getHue().doubleValue() * HUE_FACTOR); newLightState.hue = (int) (hsbCommand.getHue().doubleValue() * HUE_FACTOR);
newLightState.sat = fromPercentType(hsbCommand.getSaturation()); newLightState.sat = Util.fromPercentType(hsbCommand.getSaturation());
} }
} else if (command instanceof PercentType) { } else if (command instanceof PercentType) {
newLightState.bri = fromPercentType((PercentType) command); newLightState.bri = Util.fromPercentType((PercentType) command);
} else if (command instanceof DecimalType) { } else if (command instanceof DecimalType) {
newLightState.bri = ((DecimalType) command).intValue(); newLightState.bri = ((DecimalType) command).intValue();
} else { } else {
@ -203,7 +177,7 @@ public class LightThingHandler extends DeconzBaseThingHandler<LightMessage> {
// send on/off state together with brightness if not already set or unknown // send on/off state together with brightness if not already set or unknown
Integer newBri = newLightState.bri; Integer newBri = newLightState.bri;
if ((newBri != null) && ((currentOn == null) || ((newBri > 0) != currentOn))) { if (newBri != null) {
newLightState.on = (newBri > 0); newLightState.on = (newBri > 0);
} }
@ -222,13 +196,7 @@ public class LightThingHandler extends DeconzBaseThingHandler<LightMessage> {
if (command instanceof DecimalType) { if (command instanceof DecimalType) {
int miredValue = kelvinToMired(((DecimalType) command).intValue()); int miredValue = kelvinToMired(((DecimalType) command).intValue());
newLightState.ct = constrainToRange(miredValue, ctMin, ctMax); newLightState.ct = constrainToRange(miredValue, ctMin, ctMax);
newLightState.on = true;
if (currentOn != null && !currentOn) {
// sending new color temperature is only allowed when light is on
newLightState.on = true;
}
} else {
return;
} }
break; break;
case CHANNEL_POSITION: case CHANNEL_POSITION:
@ -253,31 +221,17 @@ public class LightThingHandler extends DeconzBaseThingHandler<LightMessage> {
return; return;
} }
AsyncHttpClient asyncHttpClient = http;
if (asyncHttpClient == null) {
return;
}
String url = buildUrl(bridgeConfig.host, bridgeConfig.httpPort, bridgeConfig.apikey, "lights", config.id,
"state");
if (newLightState.on != null && !newLightState.on) { if (newLightState.on != null && !newLightState.on) {
// if light shall be off, no other commands are allowed, so reset the new light state // if light shall be off, no other commands are allowed, so reset the new light state
newLightState.clear(); newLightState.clear();
newLightState.on = false; newLightState.on = false;
} }
String json = gson.toJson(newLightState); sendCommand(newLightState, command, channelUID, () -> {
logger.trace("Sending {} to light {} via {}", json, config.id, url);
asyncHttpClient.put(url, json, bridgeConfig.timeout).thenAccept(v -> {
lastCommandExpireTimestamp = System.currentTimeMillis() lastCommandExpireTimestamp = System.currentTimeMillis()
+ (newLightState.transitiontime != null ? newLightState.transitiontime + (newLightState.transitiontime != null ? newLightState.transitiontime
: DEFAULT_COMMAND_EXPIRY_TIME); : DEFAULT_COMMAND_EXPIRY_TIME);
lastCommand = newLightState; lastCommand = newLightState;
logger.trace("Result code={}, body={}", v.getResponseCode(), v.getBody());
}).exceptionally(e -> {
logger.debug("Sending command {} to channel {} failed:", command, channelUID, e);
return null;
}); });
} }
@ -389,19 +343,4 @@ public class LightThingHandler extends DeconzBaseThingHandler<LightMessage> {
} }
} }
} }
private PercentType toPercentType(int val) {
int scaledValue = (int) Math.ceil(val / BRIGHTNESS_FACTOR);
if (scaledValue < 0 || scaledValue > 100) {
logger.trace("received value {} (converted to {}). Coercing.", val, scaledValue);
scaledValue = scaledValue < 0 ? 0 : scaledValue;
scaledValue = scaledValue > 100 ? 100 : scaledValue;
}
logger.debug("val = '{}', scaledValue = '{}'", val, scaledValue);
return new PercentType(scaledValue);
}
private int fromPercentType(PercentType val) {
return (int) Math.floor(val.doubleValue() * BRIGHTNESS_FACTOR);
}
} }

View File

@ -29,7 +29,7 @@ import org.openhab.binding.deconz.internal.dto.SensorConfig;
import org.openhab.binding.deconz.internal.dto.SensorMessage; import org.openhab.binding.deconz.internal.dto.SensorMessage;
import org.openhab.binding.deconz.internal.dto.SensorState; import org.openhab.binding.deconz.internal.dto.SensorState;
import org.openhab.binding.deconz.internal.netutils.AsyncHttpClient; import org.openhab.binding.deconz.internal.netutils.AsyncHttpClient;
import org.openhab.binding.deconz.internal.netutils.WebSocketConnection; import org.openhab.binding.deconz.internal.types.ResourceType;
import org.openhab.core.library.types.DecimalType; import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.OnOffType; import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.QuantityType; import org.openhab.core.library.types.QuantityType;
@ -77,28 +77,7 @@ public abstract class SensorBaseThingHandler extends DeconzBaseThingHandler<Sens
private @Nullable ScheduledFuture<?> lastSeenPollingJob; private @Nullable ScheduledFuture<?> lastSeenPollingJob;
public SensorBaseThingHandler(Thing thing, Gson gson) { public SensorBaseThingHandler(Thing thing, Gson gson) {
super(thing, gson); super(thing, gson, ResourceType.SENSORS);
}
@Override
protected void requestState() {
requestState("sensors");
}
@Override
protected void registerListener() {
WebSocketConnection conn = connection;
if (conn != null) {
conn.registerSensorListener(config.id, this);
}
}
@Override
protected void unregisterListener() {
WebSocketConnection conn = connection;
if (conn != null) {
conn.unregisterSensorListener(config.id);
}
} }
@Override @Override

View File

@ -13,7 +13,6 @@
package org.openhab.binding.deconz.internal.handler; package org.openhab.binding.deconz.internal.handler;
import static org.openhab.binding.deconz.internal.BindingConstants.*; import static org.openhab.binding.deconz.internal.BindingConstants.*;
import static org.openhab.binding.deconz.internal.Util.buildUrl;
import static org.openhab.core.library.unit.SIUnits.CELSIUS; import static org.openhab.core.library.unit.SIUnits.CELSIUS;
import static org.openhab.core.library.unit.SmartHomeUnits.PERCENT; import static org.openhab.core.library.unit.SmartHomeUnits.PERCENT;
@ -28,7 +27,6 @@ import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.deconz.internal.dto.SensorConfig; import org.openhab.binding.deconz.internal.dto.SensorConfig;
import org.openhab.binding.deconz.internal.dto.SensorState; import org.openhab.binding.deconz.internal.dto.SensorState;
import org.openhab.binding.deconz.internal.dto.ThermostatConfig; import org.openhab.binding.deconz.internal.dto.ThermostatConfig;
import org.openhab.binding.deconz.internal.netutils.AsyncHttpClient;
import org.openhab.binding.deconz.internal.types.ThermostatMode; import org.openhab.binding.deconz.internal.types.ThermostatMode;
import org.openhab.core.library.types.DecimalType; import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.QuantityType; import org.openhab.core.library.types.QuantityType;
@ -121,26 +119,7 @@ public class SensorThermostatThingHandler extends SensorBaseThingHandler {
} }
AsyncHttpClient asyncHttpClient = http; sendCommand(newConfig, command, channelUID, null);
if (asyncHttpClient == null) {
return;
}
String url = buildUrl(bridgeConfig.host, bridgeConfig.httpPort, bridgeConfig.apikey, "sensors", config.id,
"config");
String json = gson.toJson(newConfig);
logger.trace("Sending {} to sensor {} via {}", json, config.id, url);
asyncHttpClient.put(url, json, bridgeConfig.timeout).thenAccept(v -> {
String bodyContent = v.getBody();
logger.trace("Result code={}, body={}", v.getResponseCode(), bodyContent);
if (!bodyContent.contains("success")) {
logger.debug("Sending command {} to channel {} failed: {}", command, channelUID, bodyContent);
}
}).exceptionally(e -> {
logger.debug("Sending command {} to channel {} failed:", command, channelUID, e);
return null;
});
} }
@Override @Override

View File

@ -18,16 +18,12 @@ import static org.openhab.core.library.unit.SIUnits.*;
import static org.openhab.core.library.unit.SmartHomeUnits.*; import static org.openhab.core.library.unit.SmartHomeUnits.*;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Set; 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.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.deconz.internal.dto.SensorConfig; import org.openhab.binding.deconz.internal.dto.*;
import org.openhab.binding.deconz.internal.dto.SensorState;
import org.openhab.core.library.types.HSBType; import org.openhab.core.library.types.HSBType;
import org.openhab.core.library.types.OpenClosedType; import org.openhab.core.library.types.OpenClosedType;
import org.openhab.core.library.types.QuantityType; import org.openhab.core.library.types.QuantityType;
@ -59,13 +55,12 @@ import com.google.gson.Gson;
*/ */
@NonNullByDefault @NonNullByDefault
public class SensorThingHandler extends SensorBaseThingHandler { public class SensorThingHandler extends SensorBaseThingHandler {
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Collections public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Set.of(THING_TYPE_PRESENCE_SENSOR,
.unmodifiableSet(Stream.of(THING_TYPE_PRESENCE_SENSOR, THING_TYPE_DAYLIGHT_SENSOR, THING_TYPE_POWER_SENSOR, THING_TYPE_DAYLIGHT_SENSOR, THING_TYPE_POWER_SENSOR, THING_TYPE_CONSUMPTION_SENSOR, THING_TYPE_LIGHT_SENSOR,
THING_TYPE_CONSUMPTION_SENSOR, THING_TYPE_LIGHT_SENSOR, THING_TYPE_TEMPERATURE_SENSOR, THING_TYPE_TEMPERATURE_SENSOR, THING_TYPE_HUMIDITY_SENSOR, THING_TYPE_PRESSURE_SENSOR, THING_TYPE_SWITCH,
THING_TYPE_HUMIDITY_SENSOR, THING_TYPE_PRESSURE_SENSOR, THING_TYPE_SWITCH, THING_TYPE_OPENCLOSE_SENSOR, THING_TYPE_WATERLEAKAGE_SENSOR, THING_TYPE_FIRE_SENSOR,
THING_TYPE_OPENCLOSE_SENSOR, THING_TYPE_WATERLEAKAGE_SENSOR, THING_TYPE_FIRE_SENSOR, THING_TYPE_ALARM_SENSOR, THING_TYPE_VIBRATION_SENSOR, THING_TYPE_BATTERY_SENSOR,
THING_TYPE_ALARM_SENSOR, THING_TYPE_VIBRATION_SENSOR, THING_TYPE_BATTERY_SENSOR, THING_TYPE_CARBONMONOXIDE_SENSOR, THING_TYPE_COLOR_CONTROL);
THING_TYPE_CARBONMONOXIDE_SENSOR, THING_TYPE_COLOR_CONTROL).collect(Collectors.toSet()));
private static final List<String> CONFIG_CHANNELS = Arrays.asList(CHANNEL_BATTERY_LEVEL, CHANNEL_BATTERY_LOW, private static final List<String> CONFIG_CHANNELS = Arrays.asList(CHANNEL_BATTERY_LEVEL, CHANNEL_BATTERY_LOW,
CHANNEL_TEMPERATURE); CHANNEL_TEMPERATURE);

View File

@ -25,8 +25,10 @@ import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage;
import org.eclipse.jetty.websocket.api.annotations.WebSocket; import org.eclipse.jetty.websocket.api.annotations.WebSocket;
import org.eclipse.jetty.websocket.client.WebSocketClient; import org.eclipse.jetty.websocket.client.WebSocketClient;
import org.openhab.binding.deconz.internal.dto.DeconzBaseMessage; import org.openhab.binding.deconz.internal.dto.DeconzBaseMessage;
import org.openhab.binding.deconz.internal.dto.GroupMessage;
import org.openhab.binding.deconz.internal.dto.LightMessage; import org.openhab.binding.deconz.internal.dto.LightMessage;
import org.openhab.binding.deconz.internal.dto.SensorMessage; import org.openhab.binding.deconz.internal.dto.SensorMessage;
import org.openhab.binding.deconz.internal.types.ResourceType;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -42,12 +44,16 @@ import com.google.gson.Gson;
@WebSocket @WebSocket
@NonNullByDefault @NonNullByDefault
public class WebSocketConnection { public class WebSocketConnection {
private static final Map<ResourceType, Class<? extends DeconzBaseMessage>> EXPECTED_MESSAGE_TYPES = Map.of(
ResourceType.GROUPS, GroupMessage.class, ResourceType.LIGHTS, LightMessage.class, ResourceType.SENSORS,
SensorMessage.class);
private final Logger logger = LoggerFactory.getLogger(WebSocketConnection.class); private final Logger logger = LoggerFactory.getLogger(WebSocketConnection.class);
private final WebSocketClient client; private final WebSocketClient client;
private final WebSocketConnectionListener connectionListener; private final WebSocketConnectionListener connectionListener;
private final Map<String, WebSocketMessageListener> sensorListener = new ConcurrentHashMap<>(); private final Map<Map.Entry<ResourceType, String>, WebSocketMessageListener> listeners = new ConcurrentHashMap<>();
private final Map<String, WebSocketMessageListener> lightListener = new ConcurrentHashMap<>();
private final Gson gson; private final Gson gson;
private boolean connected = false; private boolean connected = false;
@ -84,20 +90,12 @@ public class WebSocketConnection {
client.destroy(); client.destroy();
} }
public void registerSensorListener(String sensorID, WebSocketMessageListener listener) { public void registerListener(ResourceType resourceType, String sensorID, WebSocketMessageListener listener) {
sensorListener.put(sensorID, listener); listeners.put(Map.entry(resourceType, sensorID), listener);
} }
public void unregisterSensorListener(String sensorID) { public void unregisterListener(ResourceType resourceType, String sensorID) {
sensorListener.remove(sensorID); listeners.remove(Map.entry(resourceType, sensorID));
}
public void registerLightListener(String lightID, WebSocketMessageListener listener) {
lightListener.put(lightID, listener);
}
public void unregisterLightListener(String lightID) {
sensorListener.remove(lightID);
} }
@OnWebSocketConnect @OnWebSocketConnect
@ -111,27 +109,29 @@ public class WebSocketConnection {
@OnWebSocketMessage @OnWebSocketMessage
public void onMessage(String message) { public void onMessage(String message) {
logger.trace("Raw data received by websocket: {}", message); logger.trace("Raw data received by websocket: {}", message);
DeconzBaseMessage changedMessage = gson.fromJson(message, DeconzBaseMessage.class); DeconzBaseMessage changedMessage = gson.fromJson(message, DeconzBaseMessage.class);
switch (changedMessage.r) { if (changedMessage.r == ResourceType.UNKNOWN) {
case "sensors": logger.trace("Received message has unknown resource type. Skipping message.");
WebSocketMessageListener listener = sensorListener.get(changedMessage.id); return;
if (listener != null) {
listener.messageReceived(changedMessage.id, gson.fromJson(message, SensorMessage.class));
} else {
logger.trace("Couldn't find sensor listener for id {}", changedMessage.id);
}
break;
case "lights":
listener = lightListener.get(changedMessage.id);
if (listener != null) {
listener.messageReceived(changedMessage.id, gson.fromJson(message, LightMessage.class));
} else {
logger.trace("Couldn't find light listener for id {}", changedMessage.id);
}
break;
default:
logger.debug("Unknown message type: {}", changedMessage.r);
} }
WebSocketMessageListener listener = listeners.get(Map.entry(changedMessage.r, changedMessage.id));
if (listener == null) {
logger.debug(
"Couldn't find listener for id {} with resource type {}. Either no thing for this id has been defined or this is a bug.",
changedMessage.id, changedMessage.r);
return;
}
Class<? extends DeconzBaseMessage> expectedMessageType = EXPECTED_MESSAGE_TYPES.get(changedMessage.r);
if (expectedMessageType == null) {
logger.warn("BUG! Could not get expected message type for resource type {}. Please report this incident.",
changedMessage.r);
return;
}
listener.messageReceived(changedMessage.id, gson.fromJson(message, expectedMessageType));
} }
@OnWebSocketError @OnWebSocketError

View File

@ -28,6 +28,5 @@ public interface WebSocketMessageListener {
* @param sensorID The sensor ID (API endpoint) * @param sensorID The sensor ID (API endpoint)
* @param message The received message * @param message The received message
*/ */
void messageReceived(String sensorID, DeconzBaseMessage message); void messageReceived(String sensorID, DeconzBaseMessage message);
} }

View File

@ -0,0 +1,50 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.deconz.internal.types;
import java.util.Arrays;
import java.util.Map;
import java.util.stream.Collectors;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Type of a group as reported by the REST API for usage in {@link org.openhab.binding.deconz.internal.dto.LightMessage}
*
* @author Jan N. Klug - Initial contribution
*/
@NonNullByDefault
public enum GroupType {
LIGHT_GROUP("LightGroup"),
UNKNOWN("");
private static final Map<String, GroupType> MAPPING = Arrays.stream(GroupType.values())
.collect(Collectors.toMap(v -> v.type, v -> v));
private static final Logger LOGGER = LoggerFactory.getLogger(GroupType.class);
private String type;
GroupType(String type) {
this.type = type;
}
public static GroupType fromString(String s) {
GroupType lightType = MAPPING.getOrDefault(s, UNKNOWN);
if (lightType == UNKNOWN) {
LOGGER.debug("Unknown group type '{}' found. This should be reported.", s);
}
return lightType;
}
}

View File

@ -0,0 +1,38 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.deconz.internal.types;
import java.lang.reflect.Type;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonParseException;
/**
* Custom deserializer for {@link GroupType}
*
* @author Jan N. Klug - Initial contribution
*/
@NonNullByDefault
public class GroupTypeDeserializer implements JsonDeserializer<GroupType> {
@Override
public GroupType deserialize(@Nullable JsonElement json, @Nullable Type typeOfT,
@Nullable JsonDeserializationContext context) throws JsonParseException {
String s = json != null ? json.getAsString() : null;
return s == null ? GroupType.UNKNOWN : GroupType.fromString(s);
}
}

View File

@ -14,6 +14,9 @@ package org.openhab.binding.deconz.internal.types;
import java.lang.reflect.Type; import java.lang.reflect.Type;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import com.google.gson.JsonDeserializationContext; import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer; import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement; import com.google.gson.JsonElement;
@ -24,11 +27,12 @@ import com.google.gson.JsonParseException;
* *
* @author Jan N. Klug - Initial contribution * @author Jan N. Klug - Initial contribution
*/ */
@NonNullByDefault
public class LightTypeDeserializer implements JsonDeserializer<LightType> { public class LightTypeDeserializer implements JsonDeserializer<LightType> {
@Override @Override
public LightType deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) public LightType deserialize(@Nullable JsonElement json, @Nullable Type typeOfT,
throws JsonParseException { @Nullable JsonDeserializationContext context) throws JsonParseException {
String s = json.getAsString(); String s = json != null ? json.getAsString() : null;
return s == null ? LightType.UNKNOWN : LightType.fromString(s); return s == null ? LightType.UNKNOWN : LightType.fromString(s);
} }
} }

View File

@ -0,0 +1,78 @@
/**
* 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.deconz.internal.types;
import java.util.Arrays;
import java.util.Map;
import java.util.stream.Collectors;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link ResourceType} defines an enum for websocket messages
*
* @author Jan N. Klug - Initial contribution
*/
@NonNullByDefault
public enum ResourceType {
GROUPS("groups", "action"),
LIGHTS("lights", "state"),
SENSORS("sensors", ""),
UNKNOWN("", "");
private static final Map<String, ResourceType> MAPPING = Arrays.stream(ResourceType.values())
.collect(Collectors.toMap(v -> v.identifier, v -> v));
private static final Logger LOGGER = LoggerFactory.getLogger(ResourceType.class);
private String identifier;
private String commandUrl;
ResourceType(String identifier, String commandUrl) {
this.identifier = identifier;
this.commandUrl = commandUrl;
}
/**
* get the identifier string of this resource type
*
* @return
*/
public String getIdentifier() {
return identifier;
}
/**
* get the commandUrl part for this resource type
*
* @return
*/
public String getCommandUrl() {
return commandUrl;
}
/**
* get the resource type from a string
*
* @param s the string
* @return the corresponding resource type (or UNKNOWN)
*/
public static ResourceType fromString(String s) {
ResourceType lightType = MAPPING.getOrDefault(s, UNKNOWN);
if (lightType == UNKNOWN) {
LOGGER.debug("Unknown resource type '{}' found. This should be reported.", s);
}
return lightType;
}
}

View File

@ -0,0 +1,38 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.deconz.internal.types;
import java.lang.reflect.Type;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonParseException;
/**
* Custom deserializer for {@link ResourceType}
*
* @author Jan N. Klug - Initial contribution
*/
@NonNullByDefault
public class ResourceTypeDeserializer implements JsonDeserializer<ResourceType> {
@Override
public ResourceType deserialize(@Nullable JsonElement json, @Nullable Type typeOfT,
@Nullable JsonDeserializationContext context) throws JsonParseException {
String s = json != null ? json.getAsString() : null;
return s == null ? ResourceType.UNKNOWN : ResourceType.fromString(s);
}
}

View File

@ -0,0 +1,54 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="deconz"
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">
<thing-type id="lightgroup">
<supported-bridge-type-refs>
<bridge-type-ref id="deconz"/>
</supported-bridge-type-refs>
<label>Light Group</label>
<channels>
<channel typeId="all_on" id="all_on"/>
<channel typeId="any_on" id="any_on"/>
<channel typeId="alert" id="alert"/>
<channel typeId="color" id="color"/>
<channel typeId="ct" id="color_temperature"/>
</channels>
<representation-property>uid</representation-property>
<config-description-ref uri="thing-type:deconz:sensor"/>
</thing-type>
<channel-type id="alert">
<item-type>Switch</item-type>
<label>Alert</label>
</channel-type>
<channel-type id="all_on">
<item-type>Switch</item-type>
<label>All On</label>
<description>"On" if all lights in this group are "On", otherwise "Off".</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="any_on">
<item-type>Switch</item-type>
<label>Any On</label>
<description>"On" if any light in this group is "On", otherwise "Off".</description>
</channel-type>
<channel-type id="color">
<item-type>Color</item-type>
<label>Color</label>
</channel-type>
<channel-type id="ct">
<item-type>Number</item-type>
<label>Color Temperature</label>
<state pattern="%d K" min="15" max="100000" step="100"/>
</channel-type>
</thing:thing-descriptions>

View File

@ -132,7 +132,7 @@
<channel-type id="ct"> <channel-type id="ct">
<item-type>Number</item-type> <item-type>Number</item-type>
<label>Color Temperature</label> <label>Color Temperature</label>
<state pattern="%d" min="15" max="100000" step="100"/> <state pattern="%d K" min="15" max="100000" step="100"/>
</channel-type> </channel-type>
<channel-type id="alert"> <channel-type id="alert">

View File

@ -34,10 +34,7 @@ import org.openhab.binding.deconz.internal.Util;
import org.openhab.binding.deconz.internal.discovery.ThingDiscoveryService; import org.openhab.binding.deconz.internal.discovery.ThingDiscoveryService;
import org.openhab.binding.deconz.internal.dto.BridgeFullState; import org.openhab.binding.deconz.internal.dto.BridgeFullState;
import org.openhab.binding.deconz.internal.handler.DeconzBridgeHandler; import org.openhab.binding.deconz.internal.handler.DeconzBridgeHandler;
import org.openhab.binding.deconz.internal.types.LightType; import org.openhab.binding.deconz.internal.types.*;
import org.openhab.binding.deconz.internal.types.LightTypeDeserializer;
import org.openhab.binding.deconz.internal.types.ThermostatMode;
import org.openhab.binding.deconz.internal.types.ThermostatModeGsonTypeAdapter;
import org.openhab.core.config.discovery.DiscoveryListener; import org.openhab.core.config.discovery.DiscoveryListener;
import org.openhab.core.library.types.DateTimeType; import org.openhab.core.library.types.DateTimeType;
import org.openhab.core.thing.Bridge; import org.openhab.core.thing.Bridge;
@ -68,6 +65,8 @@ public class DeconzTest {
GsonBuilder gsonBuilder = new GsonBuilder(); GsonBuilder gsonBuilder = new GsonBuilder();
gsonBuilder.registerTypeAdapter(LightType.class, new LightTypeDeserializer()); gsonBuilder.registerTypeAdapter(LightType.class, new LightTypeDeserializer());
gsonBuilder.registerTypeAdapter(GroupType.class, new GroupTypeDeserializer());
gsonBuilder.registerTypeAdapter(ResourceType.class, new ResourceTypeDeserializer());
gsonBuilder.registerTypeAdapter(ThermostatMode.class, new ThermostatModeGsonTypeAdapter()); gsonBuilder.registerTypeAdapter(ThermostatMode.class, new ThermostatModeGsonTypeAdapter());
gson = gsonBuilder.create(); gson = gsonBuilder.create();
} }
@ -84,7 +83,7 @@ public class DeconzTest {
discoveryService.addDiscoveryListener(discoveryListener); discoveryService.addDiscoveryListener(discoveryListener);
discoveryService.stateRequestFinished(bridgeFullState); discoveryService.stateRequestFinished(bridgeFullState);
Mockito.verify(discoveryListener, times(15)).thingDiscovered(any(), any()); Mockito.verify(discoveryListener, times(20)).thingDiscovered(any(), any());
} }
public static <T> T getObjectFromJson(String filename, Class<T> clazz, Gson gson) throws IOException { public static <T> T getObjectFromJson(String filename, Class<T> clazz, Gson gson) throws IOException {