/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
| 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
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:
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
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" ]
}
```
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
*/
@NonNullByDefault
public class BindingConstants {
-
public static final String BINDING_ID = "deconz";
// List of all Thing Type UIDs
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";
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";
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;
}
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;
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;
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();
}
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;
*/
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;
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
*/
@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://");
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
*
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;
}
/**
- * Add a sensor device to the discovery inbox.
+ * Add a group to the discovery inbox.
+ *
+ * @param groupId The id of the light
+ * @param group The group description
+ */
+ 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 sensor description
+ * @param lightId The id of the light
+ * @param light The light description
*/
- private void addLight(String lightID, LightMessage light) {
+ 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.");
}
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);
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);
}
if (fullState != null) {
fullState.sensors.forEach(this::addSensor);
fullState.lights.forEach(this::addLight);
+ fullState.groups.forEach(this::addGroup);
}
}
}
public Map<String, SensorMessage> sensors = Collections.emptyMap();
public Map<String, LightMessage> lights = Collections.emptyMap();
+ public Map<String, GroupMessage> groups = Collections.emptyMap();
}
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.
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"
--- /dev/null
+/**
+ * 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 + '}';
+ }
+}
--- /dev/null
+/**
+ * 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 + '}';
+ }
+}
--- /dev/null
+/**
+ * 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 + '}';
+ }
+}
--- /dev/null
+/**
+ * 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 = "";
+}
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;
*
* 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
*/
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;
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;
}
/**
}
}
- 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) {
return;
}
- if (bridgeStatusInfo.getStatus() == ThingStatus.OFFLINE) {
- updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_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;
- }
+ 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();
+ final WebSocketConnection webSocketConnection = bridgeHandler.getWebsocketConnection();
+ this.connection = webSocketConnection;
+ this.http = bridgeHandler.getHttp();
+ this.bridgeConfig = bridgeHandler.getBridgeConfig();
- updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.NONE);
+ updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.NONE);
- // Real-time data
- registerListener();
+ // Real-time data
+ registerListener();
- // get initial values
- requestState();
+ // 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();
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
+ }
}
protected abstract @Nullable T parseStateResponse(AsyncHttpClient.Result r);
*/
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
}).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();
}
--- /dev/null
+/**
+ * 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;
+ }
+ }
+ }
+}
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
-import java.util.stream.Collectors;
-import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.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;
*/
@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);
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;
}
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) {
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 {
// 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);
}
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:
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;
});
}
}
}
}
-
- 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);
- }
}
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;
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
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;
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;
}
- 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
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;
*/
@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);
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;
@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;
client.destroy();
}
- public void registerSensorListener(String sensorID, WebSocketMessageListener listener) {
- sensorListener.put(sensorID, listener);
- }
-
- public void unregisterSensorListener(String sensorID) {
- sensorListener.remove(sensorID);
+ public void registerListener(ResourceType resourceType, String sensorID, WebSocketMessageListener listener) {
+ listeners.put(Map.entry(resourceType, sensorID), listener);
}
- 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
@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
* @param sensorID The sensor ID (API endpoint)
* @param message The received message
*/
-
void messageReceived(String sensorID, DeconzBaseMessage message);
}
--- /dev/null
+/**
+ * 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;
+ }
+}
--- /dev/null
+/**
+ * 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);
+ }
+}
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;
*
* @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);
}
}
--- /dev/null
+/**
+ * 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;
+ }
+}
--- /dev/null
+/**
+ * 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);
+ }
+}
--- /dev/null
+<?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>
<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">
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;
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();
}
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 {