[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.danfossairunit/ @pravussum
/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.digiplex/ @rmichalak
/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` |
| Warning Device (Siren) | Warning device | `warningdevice` |
Currently only light-groups are supported via the thing-type `lightgroup`.
## Discovery
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.
## 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.
The default-value is `0`, which means "no polling at all".
`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 resolution provided is 1/10s.
If no value is provided, the default value of the device is used.
### 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:
@ -154,18 +154,23 @@ The `last_seen` channel is added when it is available AND the `lastSeenPolling`
Other devices support
| Channel Type ID | Item Type | Access Mode | Description | Thing types |
|-------------------|--------------------------|:-----------:|---------------------------------------|-----------------------------------------------|
| brightness | Dimmer | R/W | Brightness of the light | `dimmablelight`, `colortemperaturelight` |
| switch | Switch | R/W | State of a ON/OFF device | `onofflight` |
| color | Color | R/W | Color of an multi-color light | `colorlight`, `extendedcolorlight` |
| color_temperature | Number | R/W | Color temperature in kelvin. The value range is determined by each individual light | `colortemperaturelight`, `extendedcolorlight` |
| position | Rollershutter | R/W | Position of the blind | `windowcovering` |
| heatsetpoint | Number:Temperature | R/W | Target Temperature in °C | `thermostat` |
| valve | Number:Dimensionless | R | Valve position in % | `thermostat` |
| mode | String | R/W | Mode: "auto", "heat" and "off" | `thermostat` |
| offset | Number | R | Temperature offset for sensor | `thermostat` |
| alert | Switch | R/W | Turn alerts on/off | `warningdevice` |
| Channel Type ID | Item Type | Access Mode | Description | Thing types |
|-------------------|--------------------------|:-----------:|---------------------------------------|-------------------------------------------------|
| brightness | Dimmer | R/W | Brightness of the light | `dimmablelight`, `colortemperaturelight` |
| switch | Switch | R/W | State of a ON/OFF device | `onofflight` |
| 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`, `lightgroup` |
| position | Rollershutter | R/W | Position of the blind | `windowcovering` |
| heatsetpoint | Number:Temperature | R/W | Target Temperature in °C | `thermostat` |
| valve | Number:Dimensionless | R | Valve position in % | `thermostat` |
| mode | String | R/W | Mode: "auto", "heat" and "off" | `thermostat` |
| offset | Number | R | Temperature offset for sensor | `thermostat` |
| 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
@ -207,6 +212,7 @@ Bridge deconz:deconz:homeserver [ host="192.168.0.10", apikey="ABCDEFGHIJ" ] {
waterleakagesensor basement-water-leakage "Basement Water Leakage" [ id="7" ]
alarmsensor basement-alarm "Basement Alarm Sensor" [ id="8", lastSeenPolling=5 ]
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_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" }
Color Livingroom "Livingroom Light Control"
```
### Events

View File

@ -23,7 +23,6 @@ import org.openhab.core.thing.ThingTypeUID;
*/
@NonNullByDefault
public class BindingConstants {
public static final String BINDING_ID = "deconz";
// 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_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_LAST_UPDATED = "last_updated";
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_VALVE_POSITION = "valve";
// group + light channel ids
public static final String CHANNEL_SWITCH = "switch";
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_ALERT = "alert";
public static final String CHANNEL_ALL_ON = "all_on";
public static final String CHANNEL_ANY_ON = "any_on";
// Thing configuration
public static final String CONFIG_HOST = "host";
public static final String CONFIG_HTTP_PORT = "httpPort";
public static final String CONFIG_APIKEY = "apikey";
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 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_MAX = 65279; // 0xFEFF
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.Nullable;
import org.openhab.binding.deconz.internal.handler.DeconzBridgeHandler;
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.handler.*;
import org.openhab.binding.deconz.internal.netutils.AsyncHttpClient;
import org.openhab.binding.deconz.internal.types.LightType;
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.binding.deconz.internal.types.*;
import org.openhab.core.io.net.http.HttpClientFactory;
import org.openhab.core.io.net.http.WebSocketFactory;
import org.openhab.core.thing.Bridge;
@ -53,7 +47,8 @@ import com.google.gson.GsonBuilder;
public class DeconzHandlerFactory extends BaseThingHandlerFactory {
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Stream
.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());
private final Gson gson;
@ -71,6 +66,8 @@ public class DeconzHandlerFactory extends BaseThingHandlerFactory {
GsonBuilder gsonBuilder = new GsonBuilder();
gsonBuilder.registerTypeAdapter(LightType.class, new LightTypeDeserializer());
gsonBuilder.registerTypeAdapter(GroupType.class, new GroupTypeDeserializer());
gsonBuilder.registerTypeAdapter(ResourceType.class, new ResourceTypeDeserializer());
gsonBuilder.registerTypeAdapter(ThermostatMode.class, new ThermostatModeGsonTypeAdapter());
gson = gsonBuilder.create();
}
@ -93,6 +90,8 @@ public class DeconzHandlerFactory extends BaseThingHandlerFactory {
return new SensorThingHandler(thing, gson);
} else if (SensorThermostatThingHandler.SUPPORTED_THING_TYPES.contains(thingTypeUID)) {
return new SensorThermostatThingHandler(thing, gson);
} else if (GroupThingHandler.SUPPORTED_THING_TYPE_UIDS.contains(thingTypeUID)) {
return new GroupThingHandler(thing, gson);
}
return null;

View File

@ -12,6 +12,8 @@
*/
package org.openhab.binding.deconz.internal;
import static org.openhab.binding.deconz.internal.BindingConstants.BRIGHTNESS_FACTOR;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZoneOffset;
@ -22,6 +24,9 @@ import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault;
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
@ -30,6 +35,8 @@ import org.openhab.core.library.types.DateTimeType;
*/
@NonNullByDefault
public class Util {
private static final Logger LOGGER = LoggerFactory.getLogger(Util.class);
public static String buildUrl(String host, int port, String... urlParts) {
StringBuilder url = new StringBuilder();
url.append("http://");
@ -54,6 +61,33 @@ public class Util {
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
*

View File

@ -26,12 +26,14 @@ 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.BridgeFullState;
import org.openhab.binding.deconz.internal.dto.GroupMessage;
import org.openhab.binding.deconz.internal.dto.LightMessage;
import org.openhab.binding.deconz.internal.dto.SensorMessage;
import org.openhab.binding.deconz.internal.handler.DeconzBridgeHandler;
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.types.GroupType;
import org.openhab.binding.deconz.internal.types.LightType;
import org.openhab.core.config.discovery.AbstractDiscoveryService;
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 light The sensor description
* @param groupId The id of the light
* @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;
if (bridgeUID == null) {
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<>();
properties.put("id", lightID);
properties.put(CONFIG_ID, lightId);
properties.put(UNIQUE_ID, light.uniqueid);
properties.put(Thing.PROPERTY_FIRMWARE_VERSION, light.swversion);
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\\[\\]]", ""));
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();
thingDiscovered(discoveryResult);
}
@ -268,6 +311,7 @@ public class ThingDiscoveryService extends AbstractDiscoveryService implements D
if (fullState != null) {
fullState.sensors.forEach(this::addSensor);
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, 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.Nullable;
import org.openhab.binding.deconz.internal.types.ResourceType;
/**
* The REST interface and websocket connection are using the same fields.
@ -25,7 +26,7 @@ import org.eclipse.jdt.annotation.Nullable;
public class DeconzBaseMessage {
// For websocket change events
public String e = ""; // "changed"
public String r = ""; // "sensors"
public ResourceType r = ResourceType.UNKNOWN; // "sensors"
public String t = ""; // "event"
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.WebSocketConnection;
import org.openhab.binding.deconz.internal.netutils.WebSocketMessageListener;
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.binding.deconz.internal.types.ResourceType;
import org.openhab.core.thing.*;
import org.openhab.core.thing.binding.BaseThingHandler;
import org.openhab.core.types.Command;
import org.slf4j.Logger;
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
* 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 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
implements WebSocketMessageListener {
private final Logger logger = LoggerFactory.getLogger(DeconzBaseThingHandler.class);
protected final ResourceType resourceType;
protected ThingConfig config = new ThingConfig();
protected DeconzBridgeConfig bridgeConfig = new DeconzBridgeConfig();
protected final Gson gson;
@ -59,9 +56,10 @@ public abstract class DeconzBaseThingHandler<T extends DeconzBaseMessage> extend
protected @Nullable WebSocketConnection connection;
protected @Nullable AsyncHttpClient http;
public DeconzBaseThingHandler(Thing thing, Gson gson) {
public DeconzBaseThingHandler(Thing thing, Gson gson, ResourceType resourceType) {
super(thing);
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
public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
@ -86,39 +94,38 @@ public abstract class DeconzBaseThingHandler<T extends DeconzBaseMessage> extend
return;
}
if (bridgeStatusInfo.getStatus() == ThingStatus.OFFLINE) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
if (bridgeStatusInfo.getStatus() == ThingStatus.ONLINE) {
// 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();
return;
}
if (bridgeStatusInfo.getStatus() != ThingStatus.ONLINE) {
return;
}
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();
}
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);
/**
* 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.
*/
protected void requestState(String type) {
protected void requestState() {
AsyncHttpClient asyncHttpClient = http;
if (asyncHttpClient == null) {
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);
// Get initial data
@ -165,13 +168,42 @@ public abstract class DeconzBaseThingHandler<T extends DeconzBaseMessage> extend
}).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
public void dispose() {
stopInitializationJob();
WebSocketConnection webSocketConnection = connection;
if (webSocketConnection != null) {
webSocketConnection.unregisterLightListener(config.id);
}
unregisterListener();
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.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;
@ -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.LightState;
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.HSBType;
import org.openhab.core.library.types.OnOffType;
@ -68,12 +66,10 @@ import com.google.gson.Gson;
*/
@NonNullByDefault
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_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 final Logger logger = LoggerFactory.getLogger(LightThingHandler.class);
@ -94,8 +90,7 @@ public class LightThingHandler extends DeconzBaseThingHandler<LightMessage> {
private int ctMin = ZCL_CT_MIN;
public LightThingHandler(Thing thing, Gson gson, StateDescriptionProvider stateDescriptionProvider) {
super(thing, gson);
super(thing, gson, ResourceType.LIGHTS);
this.stateDescriptionProvider = stateDescriptionProvider;
}
@ -127,27 +122,6 @@ public class LightThingHandler extends DeconzBaseThingHandler<LightMessage> {
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
public void handleCommand(ChannelUID channelUID, Command command) {
if (command instanceof RefreshType) {
@ -186,15 +160,15 @@ public class LightThingHandler extends DeconzBaseThingHandler<LightMessage> {
logger.warn("Failed to convert {} to xy-values", command);
}
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 {
// 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.sat = fromPercentType(hsbCommand.getSaturation());
newLightState.sat = Util.fromPercentType(hsbCommand.getSaturation());
}
} else if (command instanceof PercentType) {
newLightState.bri = fromPercentType((PercentType) command);
newLightState.bri = Util.fromPercentType((PercentType) command);
} else if (command instanceof DecimalType) {
newLightState.bri = ((DecimalType) command).intValue();
} else {
@ -203,7 +177,7 @@ public class LightThingHandler extends DeconzBaseThingHandler<LightMessage> {
// send on/off state together with brightness if not already set or unknown
Integer newBri = newLightState.bri;
if ((newBri != null) && ((currentOn == null) || ((newBri > 0) != currentOn))) {
if (newBri != null) {
newLightState.on = (newBri > 0);
}
@ -222,13 +196,7 @@ public class LightThingHandler extends DeconzBaseThingHandler<LightMessage> {
if (command instanceof DecimalType) {
int miredValue = kelvinToMired(((DecimalType) command).intValue());
newLightState.ct = constrainToRange(miredValue, ctMin, ctMax);
if (currentOn != null && !currentOn) {
// sending new color temperature is only allowed when light is on
newLightState.on = true;
}
} else {
return;
newLightState.on = true;
}
break;
case CHANNEL_POSITION:
@ -253,31 +221,17 @@ public class LightThingHandler extends DeconzBaseThingHandler<LightMessage> {
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 light shall be off, no other commands are allowed, so reset the new light state
newLightState.clear();
newLightState.on = false;
}
String json = gson.toJson(newLightState);
logger.trace("Sending {} to light {} via {}", json, config.id, url);
asyncHttpClient.put(url, json, bridgeConfig.timeout).thenAccept(v -> {
sendCommand(newLightState, command, channelUID, () -> {
lastCommandExpireTimestamp = System.currentTimeMillis()
+ (newLightState.transitiontime != null ? newLightState.transitiontime
: DEFAULT_COMMAND_EXPIRY_TIME);
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.SensorState;
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.OnOffType;
import org.openhab.core.library.types.QuantityType;
@ -77,28 +77,7 @@ public abstract class SensorBaseThingHandler extends DeconzBaseThingHandler<Sens
private @Nullable ScheduledFuture<?> lastSeenPollingJob;
public SensorBaseThingHandler(Thing thing, Gson gson) {
super(thing, gson);
}
@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);
}
super(thing, gson, ResourceType.SENSORS);
}
@Override

View File

@ -13,7 +13,6 @@
package org.openhab.binding.deconz.internal.handler;
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.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.SensorState;
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.core.library.types.DecimalType;
import org.openhab.core.library.types.QuantityType;
@ -121,26 +119,7 @@ public class SensorThermostatThingHandler extends SensorBaseThingHandler {
}
AsyncHttpClient asyncHttpClient = http;
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;
});
sendCommand(newConfig, command, channelUID, null);
}
@Override

View File

@ -18,16 +18,12 @@ import static org.openhab.core.library.unit.SIUnits.*;
import static org.openhab.core.library.unit.SmartHomeUnits.*;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
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.deconz.internal.dto.SensorConfig;
import org.openhab.binding.deconz.internal.dto.SensorState;
import org.openhab.binding.deconz.internal.dto.*;
import org.openhab.core.library.types.HSBType;
import org.openhab.core.library.types.OpenClosedType;
import org.openhab.core.library.types.QuantityType;
@ -59,13 +55,12 @@ import com.google.gson.Gson;
*/
@NonNullByDefault
public class SensorThingHandler extends SensorBaseThingHandler {
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Collections
.unmodifiableSet(Stream.of(THING_TYPE_PRESENCE_SENSOR, THING_TYPE_DAYLIGHT_SENSOR, THING_TYPE_POWER_SENSOR,
THING_TYPE_CONSUMPTION_SENSOR, THING_TYPE_LIGHT_SENSOR, THING_TYPE_TEMPERATURE_SENSOR,
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_ALARM_SENSOR, THING_TYPE_VIBRATION_SENSOR, THING_TYPE_BATTERY_SENSOR,
THING_TYPE_CARBONMONOXIDE_SENSOR, THING_TYPE_COLOR_CONTROL).collect(Collectors.toSet()));
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Set.of(THING_TYPE_PRESENCE_SENSOR,
THING_TYPE_DAYLIGHT_SENSOR, THING_TYPE_POWER_SENSOR, THING_TYPE_CONSUMPTION_SENSOR, THING_TYPE_LIGHT_SENSOR,
THING_TYPE_TEMPERATURE_SENSOR, 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_ALARM_SENSOR, THING_TYPE_VIBRATION_SENSOR, THING_TYPE_BATTERY_SENSOR,
THING_TYPE_CARBONMONOXIDE_SENSOR, THING_TYPE_COLOR_CONTROL);
private static final List<String> CONFIG_CHANNELS = Arrays.asList(CHANNEL_BATTERY_LEVEL, CHANNEL_BATTERY_LOW,
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.client.WebSocketClient;
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.SensorMessage;
import org.openhab.binding.deconz.internal.types.ResourceType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -42,12 +44,16 @@ import com.google.gson.Gson;
@WebSocket
@NonNullByDefault
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 WebSocketClient client;
private final WebSocketConnectionListener connectionListener;
private final Map<String, WebSocketMessageListener> sensorListener = new ConcurrentHashMap<>();
private final Map<String, WebSocketMessageListener> lightListener = new ConcurrentHashMap<>();
private final Map<Map.Entry<ResourceType, String>, WebSocketMessageListener> listeners = new ConcurrentHashMap<>();
private final Gson gson;
private boolean connected = false;
@ -84,20 +90,12 @@ public class WebSocketConnection {
client.destroy();
}
public void registerSensorListener(String sensorID, WebSocketMessageListener listener) {
sensorListener.put(sensorID, listener);
public void registerListener(ResourceType resourceType, String sensorID, WebSocketMessageListener listener) {
listeners.put(Map.entry(resourceType, sensorID), listener);
}
public void unregisterSensorListener(String sensorID) {
sensorListener.remove(sensorID);
}
public void registerLightListener(String lightID, WebSocketMessageListener listener) {
lightListener.put(lightID, listener);
}
public void unregisterLightListener(String lightID) {
sensorListener.remove(lightID);
public void unregisterListener(ResourceType resourceType, String sensorID) {
listeners.remove(Map.entry(resourceType, sensorID));
}
@OnWebSocketConnect
@ -111,27 +109,29 @@ public class WebSocketConnection {
@OnWebSocketMessage
public void onMessage(String message) {
logger.trace("Raw data received by websocket: {}", message);
DeconzBaseMessage changedMessage = gson.fromJson(message, DeconzBaseMessage.class);
switch (changedMessage.r) {
case "sensors":
WebSocketMessageListener listener = sensorListener.get(changedMessage.id);
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);
if (changedMessage.r == ResourceType.UNKNOWN) {
logger.trace("Received message has unknown resource type. Skipping message.");
return;
}
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

View File

@ -28,6 +28,5 @@ public interface WebSocketMessageListener {
* @param sensorID The sensor ID (API endpoint)
* @param message The received 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 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;
@ -24,11 +27,12 @@ import com.google.gson.JsonParseException;
*
* @author Jan N. Klug - Initial contribution
*/
@NonNullByDefault
public class LightTypeDeserializer implements JsonDeserializer<LightType> {
@Override
public LightType deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
throws JsonParseException {
String s = json.getAsString();
public LightType deserialize(@Nullable JsonElement json, @Nullable Type typeOfT,
@Nullable JsonDeserializationContext context) throws JsonParseException {
String s = json != null ? json.getAsString() : null;
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">
<item-type>Number</item-type>
<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 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.dto.BridgeFullState;
import org.openhab.binding.deconz.internal.handler.DeconzBridgeHandler;
import org.openhab.binding.deconz.internal.types.LightType;
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.binding.deconz.internal.types.*;
import org.openhab.core.config.discovery.DiscoveryListener;
import org.openhab.core.library.types.DateTimeType;
import org.openhab.core.thing.Bridge;
@ -68,6 +65,8 @@ public class DeconzTest {
GsonBuilder gsonBuilder = new GsonBuilder();
gsonBuilder.registerTypeAdapter(LightType.class, new LightTypeDeserializer());
gsonBuilder.registerTypeAdapter(GroupType.class, new GroupTypeDeserializer());
gsonBuilder.registerTypeAdapter(ResourceType.class, new ResourceTypeDeserializer());
gsonBuilder.registerTypeAdapter(ThermostatMode.class, new ThermostatModeGsonTypeAdapter());
gson = gsonBuilder.create();
}
@ -84,7 +83,7 @@ public class DeconzTest {
discoveryService.addDiscoveryListener(discoveryListener);
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 {