diff --git a/CODEOWNERS b/CODEOWNERS index ab7d0b668..7222bf6c3 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -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 diff --git a/bundles/org.openhab.binding.deconz/README.md b/bundles/org.openhab.binding.deconz/README.md index d6a19d6f7..188da3cb0 100644 --- a/bundles/org.openhab.binding.deconz/README.md +++ b/bundles/org.openhab.binding.deconz/README.md @@ -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]" { channel="deconz:dimmablelight:homeserver:livingroom-ceiling:brightness" } +Color Livingroom "Livingroom Light Control" ``` ### Events diff --git a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/BindingConstants.java b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/BindingConstants.java index 3edf3a01a..123f86000 100644 --- a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/BindingConstants.java +++ b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/BindingConstants.java @@ -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; } diff --git a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/DeconzHandlerFactory.java b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/DeconzHandlerFactory.java index 5cda9603a..273e1b638 100644 --- a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/DeconzHandlerFactory.java +++ b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/DeconzHandlerFactory.java @@ -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 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; diff --git a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/Util.java b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/Util.java index e8379a331..3f662bdcb 100644 --- a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/Util.java +++ b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/Util.java @@ -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 * diff --git a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/discovery/ThingDiscoveryService.java b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/discovery/ThingDiscoveryService.java index 28497b70d..5cfbfeb87 100644 --- a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/discovery/ThingDiscoveryService.java +++ b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/discovery/ThingDiscoveryService.java @@ -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 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 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); } } } diff --git a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/dto/BridgeFullState.java b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/dto/BridgeFullState.java index 406f9a8e2..2aa258502 100644 --- a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/dto/BridgeFullState.java +++ b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/dto/BridgeFullState.java @@ -41,4 +41,5 @@ public class BridgeFullState { public Map sensors = Collections.emptyMap(); public Map lights = Collections.emptyMap(); + public Map groups = Collections.emptyMap(); } diff --git a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/dto/DeconzBaseMessage.java b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/dto/DeconzBaseMessage.java index c7a70c56f..2c1a263b4 100644 --- a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/dto/DeconzBaseMessage.java +++ b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/dto/DeconzBaseMessage.java @@ -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" diff --git a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/dto/GroupAction.java b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/dto/GroupAction.java new file mode 100644 index 000000000..636cbbecd --- /dev/null +++ b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/dto/GroupAction.java @@ -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 + '}'; + } +} diff --git a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/dto/GroupMessage.java b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/dto/GroupMessage.java new file mode 100644 index 000000000..307d51601 --- /dev/null +++ b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/dto/GroupMessage.java @@ -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 + '}'; + } +} diff --git a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/dto/GroupState.java b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/dto/GroupState.java new file mode 100644 index 000000000..bd13812bc --- /dev/null +++ b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/dto/GroupState.java @@ -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 + '}'; + } +} diff --git a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/dto/Scene.java b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/dto/Scene.java new file mode 100644 index 000000000..f6b1911f0 --- /dev/null +++ b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/dto/Scene.java @@ -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 = ""; +} diff --git a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/handler/DeconzBaseThingHandler.java b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/handler/DeconzBaseThingHandler.java index 4ecf2f27b..8125b2cde 100644 --- a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/handler/DeconzBaseThingHandler.java +++ b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/handler/DeconzBaseThingHandler.java @@ -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 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 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 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 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 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 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(); } diff --git a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/handler/GroupThingHandler.java b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/handler/GroupThingHandler.java new file mode 100644 index 000000000..4d1acae52 --- /dev/null +++ b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/handler/GroupThingHandler.java @@ -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 { + public static final Set 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; + } + } + } +} diff --git a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/handler/LightThingHandler.java b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/handler/LightThingHandler.java index 6b966fb24..8c788d901 100644 --- a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/handler/LightThingHandler.java +++ b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/handler/LightThingHandler.java @@ -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 { - public static final Set SUPPORTED_THING_TYPE_UIDS = Stream.of(THING_TYPE_COLOR_TEMPERATURE_LIGHT, + public static final Set 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 { 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 { 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 { 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 { // 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 { 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 { 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 { } } } - - 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); - } } diff --git a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/handler/SensorBaseThingHandler.java b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/handler/SensorBaseThingHandler.java index 587e041a7..9f9ff4284 100644 --- a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/handler/SensorBaseThingHandler.java +++ b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/handler/SensorBaseThingHandler.java @@ -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 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 diff --git a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/handler/SensorThermostatThingHandler.java b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/handler/SensorThermostatThingHandler.java index dba497c2f..ee4ace7a8 100644 --- a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/handler/SensorThermostatThingHandler.java +++ b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/handler/SensorThermostatThingHandler.java @@ -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 diff --git a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/handler/SensorThingHandler.java b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/handler/SensorThingHandler.java index 1b299e739..21217ed21 100644 --- a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/handler/SensorThingHandler.java +++ b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/handler/SensorThingHandler.java @@ -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 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 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 CONFIG_CHANNELS = Arrays.asList(CHANNEL_BATTERY_LEVEL, CHANNEL_BATTERY_LOW, CHANNEL_TEMPERATURE); diff --git a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/netutils/WebSocketConnection.java b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/netutils/WebSocketConnection.java index 14773c87e..d8d7a923a 100644 --- a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/netutils/WebSocketConnection.java +++ b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/netutils/WebSocketConnection.java @@ -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> 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 sensorListener = new ConcurrentHashMap<>(); - private final Map lightListener = new ConcurrentHashMap<>(); + private final Map, 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 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 diff --git a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/netutils/WebSocketMessageListener.java b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/netutils/WebSocketMessageListener.java index fa4e13654..96f28183d 100644 --- a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/netutils/WebSocketMessageListener.java +++ b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/netutils/WebSocketMessageListener.java @@ -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); } diff --git a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/types/GroupType.java b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/types/GroupType.java new file mode 100644 index 000000000..93a87d57e --- /dev/null +++ b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/types/GroupType.java @@ -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 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; + } +} diff --git a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/types/GroupTypeDeserializer.java b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/types/GroupTypeDeserializer.java new file mode 100644 index 000000000..3bb2c1db2 --- /dev/null +++ b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/types/GroupTypeDeserializer.java @@ -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 { + @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); + } +} diff --git a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/types/LightTypeDeserializer.java b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/types/LightTypeDeserializer.java index 9e1fcc0ba..8255feb12 100644 --- a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/types/LightTypeDeserializer.java +++ b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/types/LightTypeDeserializer.java @@ -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 { @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); } } diff --git a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/types/ResourceType.java b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/types/ResourceType.java new file mode 100644 index 000000000..6a33ad00a --- /dev/null +++ b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/types/ResourceType.java @@ -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 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; + } +} diff --git a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/types/ResourceTypeDeserializer.java b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/types/ResourceTypeDeserializer.java new file mode 100644 index 000000000..b8f6e211a --- /dev/null +++ b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/types/ResourceTypeDeserializer.java @@ -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 { + @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); + } +} diff --git a/bundles/org.openhab.binding.deconz/src/main/resources/OH-INF/thing/group-thing-types.xml b/bundles/org.openhab.binding.deconz/src/main/resources/OH-INF/thing/group-thing-types.xml new file mode 100644 index 000000000..6b6cd10e4 --- /dev/null +++ b/bundles/org.openhab.binding.deconz/src/main/resources/OH-INF/thing/group-thing-types.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + uid + + + + + + Switch + + + + + Switch + + "On" if all lights in this group are "On", otherwise "Off". + + + + + Switch + + "On" if any light in this group is "On", otherwise "Off". + + + + Color + + + + + Number + + + + + diff --git a/bundles/org.openhab.binding.deconz/src/main/resources/OH-INF/thing/light-thing-types.xml b/bundles/org.openhab.binding.deconz/src/main/resources/OH-INF/thing/light-thing-types.xml index e0a635a5e..e7a3e2a5e 100644 --- a/bundles/org.openhab.binding.deconz/src/main/resources/OH-INF/thing/light-thing-types.xml +++ b/bundles/org.openhab.binding.deconz/src/main/resources/OH-INF/thing/light-thing-types.xml @@ -132,7 +132,7 @@ Number - + diff --git a/bundles/org.openhab.binding.deconz/src/test/java/org/openhab/binding/deconz/DeconzTest.java b/bundles/org.openhab.binding.deconz/src/test/java/org/openhab/binding/deconz/DeconzTest.java index fae1d38be..eb9aa34f7 100644 --- a/bundles/org.openhab.binding.deconz/src/test/java/org/openhab/binding/deconz/DeconzTest.java +++ b/bundles/org.openhab.binding.deconz/src/test/java/org/openhab/binding/deconz/DeconzTest.java @@ -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 getObjectFromJson(String filename, Class clazz, Gson gson) throws IOException {