From ee1de1186470f9602110f9e9d2eb2d2d3b037487 Mon Sep 17 00:00:00 2001 From: J-N-K Date: Sat, 18 Mar 2023 16:06:55 +0100 Subject: [PATCH] [deconz] Add Pairing/Scene actions, new devices and improve code (#14622) * port changes * update instructions * Incorporate review comments from #14134 * new improvements (mostly Java 17 changes) * further improvements Signed-off-by: Jan N. Klug --- CODEOWNERS | 2 +- bundles/org.openhab.binding.deconz/README.md | 217 ++++++++++------ .../deconz/internal/BindingConstants.java | 14 +- ...conzDynamicCommandDescriptionProvider.java | 17 +- ...DeconzDynamicStateDescriptionProvider.java | 18 +- .../openhab/binding/deconz/internal/Util.java | 21 +- .../deconz/internal/action/BridgeActions.java | 87 +++++++ .../deconz/internal/action/GroupActions.java | 156 ++++++++++++ .../discovery/BridgeDiscoveryParticipant.java | 4 +- .../discovery/ThingDiscoveryService.java | 68 +++-- .../internal/dto/DeconzBaseMessage.java | 6 +- .../deconz/internal/dto/GroupAction.java | 17 ++ .../deconz/internal/dto/GroupState.java | 10 +- .../deconz/internal/dto/LightState.java | 5 + .../deconz/internal/dto/NewSceneResponse.java | 29 +++ .../deconz/internal/dto/SensorConfig.java | 5 +- .../deconz/internal/dto/SensorState.java | 39 ++- .../internal/dto/ThermostatUpdateConfig.java | 2 + .../handler/DeconzBaseThingHandler.java | 210 +++++++++++++--- .../internal/handler/DeconzBridgeConfig.java | 3 +- .../internal/handler/DeconzBridgeHandler.java | 96 +++++--- .../internal/handler/GroupThingHandler.java | 187 +++++++++----- .../internal/handler/LightThingHandler.java | 233 ++++++++---------- .../handler/SensorBaseThingHandler.java | 156 ++++-------- .../handler/SensorThermostatThingHandler.java | 93 ++++--- .../internal/handler/SensorThingHandler.java | 175 ++++++------- .../internal/netutils/AsyncHttpClient.java | 9 +- .../netutils/WebSocketConnection.java | 88 +++++-- .../netutils/WebSocketConnectionListener.java | 4 +- .../netutils/WebSocketMessageListener.java | 3 +- .../deconz/internal/types/GroupType.java | 8 +- .../deconz/internal/types/LightType.java | 5 +- .../deconz/internal/types/ResourceType.java | 5 +- .../deconz/internal/types/ThermostatMode.java | 5 +- .../main/resources/OH-INF/config/config.xml | 84 +++++-- .../resources/OH-INF/i18n/deconz.properties | 71 ++++-- .../OH-INF/thing/group-thing-types.xml | 13 + .../OH-INF/thing/light-thing-types.xml | 41 ++- .../OH-INF/thing/sensor-thing-types.xml | 111 ++++++--- .../main/resources/OH-INF/update/update.xml | 81 ++++++ .../openhab/binding/deconz/DeconzTest.java | 2 +- .../binding/deconz/LightGroupTest.java | 89 +++++++ .../openhab/binding/deconz/LightsTest.java | 63 ++--- .../openhab/binding/deconz/SensorsTest.java | 25 +- .../org/openhab/binding/deconz/fire.json | 2 +- .../org/openhab/binding/deconz/group.json | 30 +++ .../binding/deconz/thermostat-undef.json | 27 ++ .../openhab/binding/deconz/thermostat.json | 2 +- 48 files changed, 1814 insertions(+), 824 deletions(-) create mode 100644 bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/action/BridgeActions.java create mode 100644 bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/action/GroupActions.java create mode 100644 bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/dto/NewSceneResponse.java create mode 100644 bundles/org.openhab.binding.deconz/src/main/resources/OH-INF/update/update.xml create mode 100644 bundles/org.openhab.binding.deconz/src/test/java/org/openhab/binding/deconz/LightGroupTest.java create mode 100644 bundles/org.openhab.binding.deconz/src/test/resources/org/openhab/binding/deconz/group.json create mode 100644 bundles/org.openhab.binding.deconz/src/test/resources/org/openhab/binding/deconz/thermostat-undef.json diff --git a/CODEOWNERS b/CODEOWNERS index d10b6a3c1..5be07e407 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -64,7 +64,7 @@ /bundles/org.openhab.binding.dali/ @rs22 /bundles/org.openhab.binding.danfossairunit/ @pravussum /bundles/org.openhab.binding.dbquery/ @lujop -/bundles/org.openhab.binding.deconz/ @openhab/add-ons-maintainers +/bundles/org.openhab.binding.deconz/ @J-N-K /bundles/org.openhab.binding.denonmarantz/ @jwveldhuis /bundles/org.openhab.binding.deutschebahn/ @soenkekueper /bundles/org.openhab.binding.digiplex/ @rmichalak diff --git a/bundles/org.openhab.binding.deconz/README.md b/bundles/org.openhab.binding.deconz/README.md index 13723a457..be8def99a 100644 --- a/bundles/org.openhab.binding.deconz/README.md +++ b/bundles/org.openhab.binding.deconz/README.md @@ -9,27 +9,28 @@ deCONZ offers a documented real-time channel that this binding makes use of to b There is one bridge (`deconz`) that manages the connection to the deCONZ software instance. These sensors are supported: -| Device type | Resource Type | Thing type | -|-----------------------------------|-----------------------------------|----------------------| -| Presence Sensor | ZHAPresence, CLIPPresence | `presencesensor` | -| Power Sensor | ZHAPower, CLIPPower | `powersensor` | -| Consumption Sensor | ZHAConsumption | `consumptionsensor` | -| Switch | ZHASwitch | `switch` | -| Light Sensor | ZHALightLevel | `lightsensor` | -| Temperature Sensor | ZHATemperature | `temperaturesensor` | -| Humidity Sensor | ZHAHumidity | `humiditysensor` | -| Pressure Sensor | ZHAPressure | `pressuresensor` | -| Open/Close Sensor | ZHAOpenClose | `openclosesensor` | -| Water Leakage Sensor | ZHAWater | `waterleakagesensor` | -| Alarm Sensor | ZHAAlarm | `alarmsensor` | -| Fire Sensor | ZHAFire | `firesensor` | -| Vibration Sensor | ZHAVibration | `vibrationsensor` | -| deCONZ Artificial Daylight Sensor | deCONZ specific: simulated sensor | `daylightsensor` | -| Carbon-Monoxide Sensor | ZHACarbonmonoxide | `carbonmonoxide` | -| Air quality Sensor | ZHAAirQuality | `airqualitysensor` | -| Color Controller | ZBT-Remote-ALL-RGBW | `colorcontrol` | +| Device type | Resource Type | Thing type | +|-----------------------------------|-----------------------------------|------------------------| +| Presence Sensor | ZHAPresence, CLIPPresence | `presencesensor` | +| Power Sensor | ZHAPower, CLIPPower | `powersensor` | +| Consumption Sensor | ZHAConsumption | `consumptionsensor` | +| Switch | ZHASwitch | `switch` | +| Light Sensor | ZHALightLevel | `lightsensor` | +| Temperature Sensor | ZHATemperature | `temperaturesensor` | +| Humidity Sensor | ZHAHumidity | `humiditysensor` | +| Pressure Sensor | ZHAPressure | `pressuresensor` | +| Open/Close Sensor | ZHAOpenClose | `openclosesensor` | +| Water Leakage Sensor | ZHAWater | `waterleakagesensor` | +| Alarm Sensor | ZHAAlarm | `alarmsensor` | +| Fire Sensor | ZHAFire | `firesensor` | +| Vibration Sensor | ZHAVibration | `vibrationsensor` | +| deCONZ Artificial Daylight Sensor | deCONZ specific: simulated sensor | `daylightsensor` | +| Carbon-Monoxide Sensor | ZHACarbonmonoxide | `carbonmonoxidesensor` | +| Airquality Sensor | ZHAAirquality | `airqualitysensor` | +| Moisture Sensor | ZHAMoisture | `moisturesensor` | +| Color Controller | ZBT-Remote-ALL-RGBW | `colorcontrol` | -Additionally lights, window coverings (blinds), door locks and thermostats are supported: +Additionally, lights, window coverings (blinds), door locks and thermostats are supported: | Device type | Resource Type | Thing type | |--------------------------------------|-----------------------------------------------|-------------------------| @@ -43,6 +44,8 @@ Additionally lights, window coverings (blinds), door locks and thermostats are s | Warning Device (Siren) | Warning device | `warningdevice` | | Door Lock | A remotely operatable door lock | `doorlock` | +**Note**: `windowcovering` might require updating your deCONZ software since the support changed. + Currently only light-groups are supported via the thing-type `lightgroup`. ## Discovery @@ -57,13 +60,14 @@ If your device is not discovered, please check the DEBUG log for unknown devices These configuration parameters are available: -| Parameter | Description | Type | Default | -|-----------|---------------------------------------------------------------------------------|---------|---------| -| host | Host address (hostname / ip) of deCONZ interface | string | n/a | -| httpPort | Port of deCONZ HTTP interface | string | 80 | -| port | Port of deCONZ Websocket (optional, can be filled automatically) **(Advanced)** | string | n/a | -| apikey | Authorization API key (optional, can be filled automatically) | string | n/a | -| timeout | Timeout for asynchronous HTTP requests (in milliseconds) | integer | 2000 | +| Parameter | Description | Type | Default | +|------------------|-------------------------------------------------------------------------------------------------------------------------|---------|---------| +| host | Host address (hostname / ip) of deCONZ interface | string | n/a | +| httpPort | Port of deCONZ HTTP interface | string | 80 | +| port | Port of deCONZ Websocket (optional, can be filled automatically) **(Advanced)** | string | n/a | +| apikey | Authorization API key (optional, can be filled automatically) | string | n/a | +| timeout | Timeout for asynchronous HTTP requests (in milliseconds) | integer | 2000 | +| websocketTimeout | Timeout for the websocket connection (in s). After this time, the connection is considered dead and tries to re-connect | integer | 120 | The deCONZ bridge requires the IP address or hostname as a configuration value in order for the binding to know where to access it. If needed you can specify an optional port for the HTTP interface or the Websocket. @@ -120,42 +124,45 @@ Bridge deconz:deconz:homeserver [ host="192.168.0.10", apikey="ABCDEFGHIJ" ] The sensor devices support some of the following channels: -| Channel Type ID | Item Type | Access Mode | Description | Thing types | -|-----------------|--------------------------|-------------|-------------------------------------------------------------------------------------------|---------------------------------------------------| -| presence | Switch | R | Status of presence: `ON` = presence; `OFF` = no-presence | presencesensor | -| enabled | Switch | R/W | This channel activates or deactivates the sensor | presencesensor | -| last_updated | DateTime | R | Timestamp when the sensor was last updated | all, except daylightsensor | -| last_seen | DateTime | R | Timestamp when the sensor was last seen | all, except daylightsensor | -| power | Number:Power | R | Current power usage in Watts | powersensor, sometimes for consumptionsensor | -| consumption | Number:Energy | R | Current power usage in Watts/Hour | consumptionsensor | -| voltage | Number:ElectricPotential | R | Current voltage in V | some powersensors | -| current | Number:ElectricCurrent | R | Current current in mA | some powersensors | -| button | Number | R | Last pressed button id on a switch | switch, colorcontrol | -| gesture | Number | R | A gesture that was performed with the switch | switch | -| lightlux | Number:Illuminance | R | Current light illuminance in Lux | lightsensor | -| light_level | Number | R | Current light level | lightsensor | -| dark | Switch | R | Light level is below the darkness threshold | lightsensor, sometimes for presencesensor | -| daylight | Switch | R | Light level is above the daylight threshold | lightsensor | -| temperature | Number:Temperature | R | Current temperature in ˚C | temperaturesensor, some Xiaomi sensors,thermostat | -| humidity | Number:Dimensionless | R | Current humidity in % | humiditysensor | -| pressure | Number:Pressure | R | Current pressure in hPa | pressuresensor | -| open | Contact | R | Status of contacts: `OPEN`; `CLOSED` | openclosesensor | -| waterleakage | Switch | R | Status of water leakage: `ON` = water leakage detected; `OFF` = no water leakage detected | waterleakagesensor | -| fire | Switch | R | Status of a fire: `ON` = fire was detected; `OFF` = no fire detected | firesensor | -| alarm | Switch | R | Status of an alarm: `ON` = alarm was triggered; `OFF` = no alarm | alarmsensor | -| tampered | Switch | R | Status of a zone: `ON` = zone is being tampered; `OFF` = zone is not tampered | any IAS sensor | -| vibration | Switch | R | Status of vibration: `ON` = vibration was detected; `OFF` = no vibration | alarmsensor | -| light | String | R | Light level: `Daylight`; `Sunset`; `Dark` | daylightsensor | -| value | Number | R | Sun position: `130` = dawn; `140` = sunrise; `190` = sunset; `210` = dusk | daylightsensor | -| battery_level | Number | R | Battery level (in %) | any battery-powered sensor | -| battery_low | Switch | R | Battery level low: `ON`; `OFF` | any battery-powered sensor | -| carbonmonoxide | Switch | R | `ON` = carbon monoxide detected | carbonmonoxide | -| airquality | String | R | Current air quality level | airqualitysensor | -| airqualityppb | Number:Dimensionless | R | Current air quality ppb (parts per billion) | airqualitysensor | -| color | Color | R | Color set by remote | colorcontrol | -| windowopen | Contact | R | `windowopen` status is reported by some thermostats | thermostat | +| Channel Type ID | Item Type | Access Mode | Description | Thing types | +|--------------------|--------------------------|-------------|-------------------------------------------------------------------------------------------|---------------------------------------------------| +| presence | Switch | R | Status of presence: `ON` = presence; `OFF` = no-presence | presencesensor | +| enabled | Switch | R/W | This channel activates or deactivates the sensor | presencesensor | +| last_updated | DateTime | R | Timestamp when the sensor was last updated | all, except daylightsensor | +| last_seen | DateTime | R | Timestamp when the sensor was last seen | all, except daylightsensor | +| power | Number:Power | R | Power usage in Watts | powersensor, sometimes for consumptionsensor | +| consumption | Number:Energy | R | Energy in Watt*Hour | consumptionsensor | +| voltage | Number:ElectricPotential | R | Voltage in V | some powersensors | +| current | Number:ElectricCurrent | R | Current in mA | some powersensors | +| button | Number | R | Last pressed button id on a switch | switch, colorcontrol | +| gesture | Number | R | A gesture that was performed with the switch | switch | +| lightlux | Number:Illuminance | R | Light illuminance in Lux | lightsensor | +| light_level | Number | R | Light level | lightsensor | +| dark | Switch | R | Light level is below the darkness threshold | lightsensor, sometimes for presencesensor | +| daylight | Switch | R | Light level is above the daylight threshold | lightsensor | +| temperature | Number:Temperature | R | Temperature in ˚C | temperaturesensor, some Xiaomi sensors,thermostat | +| humidity | Number:Dimensionless | R | Humidity in % | humiditysensor | +| pressure | Number:Pressure | R | Pressure in hPa | pressuresensor | +| open | Contact | R | Status of contacts: `OPEN`; `CLOSED` | openclosesensor | +| waterleakage | Switch | R | Status of water leakage: `ON` = water leakage detected; `OFF` = no water leakage detected | waterleakagesensor | +| fire | Switch | R | Status of a fire: `ON` = fire was detected; `OFF` = no fire detected | firesensor | +| alarm | Switch | R | Status of an alarm: `ON` = alarm was triggered; `OFF` = no alarm | alarmsensor | +| tampered | Switch | R | Status of a zone: `ON` = zone is being tampered; `OFF` = zone is not tampered | any IAS sensor | +| vibration | Switch | R | Status of vibration: `ON` = vibration was detected; `OFF` = no vibration | alarmsensor | +| light | String | R | Light level: `Daylight`; `Sunset`; `Dark` | daylightsensor | +| value | Number | R | Sun position: `130` = dawn; `140` = sunrise; `190` = sunset; `210` = dusk | daylightsensor | +| battery_level | Number | R | Battery level (in %) | any battery-powered sensor | +| battery_low | Switch | R | Battery level low: `ON`; `OFF` | any battery-powered sensor | +| carbonmonoxide | Switch | R | `ON` = carbon monoxide detected | carbonmonoxide | +| color | Color | R | Color set by remote | colorcontrol | +| windowopen | Contact | R | `windowopen` status is reported by some thermostats | thermostat | +| externalwindowopen | Contact | R/W | forward a status to a theromastat (some devices) | thermostat | +| locked | Switch | R/W | reports/sets the childlock on some thermostats | thermostat | +| airquality | String | R | Airquality as string | airqualitysensor | +| airqualityppb | Number:Dimensionless | R | Airquality (in parts-per-billion) | airqualitysensor | +| moisture | Number:Dimensionless | R | Moisture | moisturesensor | -**NOTE:** Beside other non mandatory channels, the `battery_level` and `battery_low` channels will be added to the Thing during runtime if the sensor is battery-powered. +**NOTE:** Beside other non-mandatory channels, the `battery_level` and `battery_low` channels will be added to the Thing during runtime if the sensor is battery-powered. The specification of your sensor depends on the deCONZ capabilities. Have a detailed look for [supported devices](https://github.com/dresden-elektronik/deconz-rest-plugin/wiki/Supported-Devices). @@ -163,25 +170,25 @@ 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 an ON/OFF device | `onofflight` | -| color | Color | R/W | Color of a 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` | -| effect | String | R/W | Effect selection. Allowed commands are set dynamically | `colorlight` | -| effectSpeed | Number | W | Effect Speed | `colorlight` | -| lock | Switch | R/W | Lock (ON) or unlock (OFF) the doorlock| `doorlock` | -| ontime | Number:Time | W | Timespan for which the light is turned on | all lights | -| 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 | String | W | Turn alerts on. Allowed commands are `none`, `select` (short blinking), `lselect` (long blinking) | `warningdevice`, `lightgroup`, `dimmablelight`, `colorlight`, `extendedcolorlight`, `colortemperaturelight` | -| all_on | Switch | R | All lights in group are on | `lightgroup` | -| any_on | Switch | R | Any light in group is on | `lightgroup` | -| scene | String | W | Recall a scene. Allowed commands are set dynamically | `lightgroup` | +| 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` | +| effect | String | R/W | Effect selection. Allowed commands are set dynamically | `colorlight` | +| effectSpeed | Number | W | Effect Speed | `colorlight` | +| lock | Switch | R/W | Lock (ON) or unlock (OFF) the doorlock | `doorlock` | +| ontime | Number:Time | W | Timespan for which the light is turned on | all lights | +| 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 | String | W | Turn alerts on. Allowed commands are `none`, `select` (short blinking), `lselect` (long blinking) | `warningdevice`, `lightgroup`, `dimmablelight`, `colorlight`, `extendedcolorlight`, `colortemperaturelight` | +| all_on | Switch | R | All lights in group are on | `lightgroup` | +| any_on | Switch | R | Any light in group is on | `lightgroup` | +| scene | String | W | Recall a scene. Allowed commands are set dynamically | `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. @@ -211,6 +218,26 @@ Both will be added during runtime if supported by the switch. | GESTURE_ROTATE_CLOCKWISE | 7 | | GESTURE_ROTATE_COUNTER_CLOCKWISE | 8 | +## Thing Actions + +Thing actions can be used to manage the network and its content. + +The `deconz` thing supports a thing action to allow new devices to join the network: + +| Action name | Input Value | Return Value | Description | +|------------------------|----------------------|--------------|----------------------------------------------------------------------------------------------------------------| +| `permitJoin(duration)` | `duration` (Integer) | - | allows new devices to join for `duration` seconds. Allowed values are 1-240, default is 120 if no value given. | + +The `lightgroup` thing supports thing actions for managing scenes: + +| Action name | Input Value | Return Value | Description | +|---------------------|-----------------|--------------|-------------------------------------------------------------------------------------------| +| `createScene(name)` | `name` (String) | `newSceneId` | Creates a new scene with the name `name` and returns the new scene's id (if successfull). | +| `deleteScene(id)` | `id` (Integer) | - | Deletes the scene with the given id. | +| `storeScene(id)` | `id` (Integer) | - | Store the current group's state as scene with the given id. | + +The return value refers to a key of the given name within the returned Map. See [example](#thing-actions-example). + ## Full Example ### Things file @@ -260,8 +287,34 @@ then end ``` +# Thing Actions Example + +:::: tabs + +::: tab JavaScript + + ```javascript + deconzActions = actions.get("deconz", "deconz:lightgroup:00212E040ED9:5"); + retVal = deconzActions.createScene("TestScene"); + deconzActions.storeScene(retVal["newSceneId"]); + ``` + +::: + +::: tab DSL + + ```java + val deconzActions = getActions("deconz", "deconz:lightgroup:00212E040ED9:5"); + var retVal = deconzActions.createScene("TestScene"); + deconzActions.storeScene(retVal.get("newSceneId")); + ``` + +::: + +:::: + ### Troubleshooting -By default state updates are ignored for 250ms after a command. +By default, state updates are ignored for 250ms after a command. If your light takes more than that to change from one state to another, you might experience a problem with jumping sliders/color pickers. In that case the `transitiontime` parameter should be changed to the desired time. 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 a7b81419a..1d8c6c58a 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 @@ -15,6 +15,7 @@ package org.openhab.binding.deconz.internal; import org.eclipse.jdt.annotation.NonNullByDefault; import org.openhab.core.library.types.PercentType; import org.openhab.core.thing.ThingTypeUID; +import org.openhab.core.thing.type.ChannelTypeUID; /** * The {@link BindingConstants} class defines common constants, which are @@ -50,6 +51,8 @@ public class BindingConstants { public static final ThingTypeUID THING_TYPE_CARBONMONOXIDE_SENSOR = new ThingTypeUID(BINDING_ID, "carbonmonoxidesensor"); public static final ThingTypeUID THING_TYPE_AIRQUALITY_SENSOR = new ThingTypeUID(BINDING_ID, "airqualitysensor"); + public static final ThingTypeUID THING_TYPE_MOISTURE_SENSOR = new ThingTypeUID(BINDING_ID, "moisturesensor"); + // Special sensor - Thermostat public static final ThingTypeUID THING_TYPE_THERMOSTAT = new ThingTypeUID(BINDING_ID, "thermostat"); @@ -75,6 +78,7 @@ public class BindingConstants { public static final String CHANNEL_LAST_SEEN = "last_seen"; public static final String CHANNEL_POWER = "power"; public static final String CHANNEL_CONSUMPTION = "consumption"; + public static final String CHANNEL_CONSUMPTION_2 = "consumption2"; public static final String CHANNEL_VOLTAGE = "voltage"; public static final String CHANNEL_CURRENT = "current"; public static final String CHANNEL_VALUE = "value"; @@ -101,11 +105,14 @@ public class BindingConstants { public static final String CHANNEL_CARBONMONOXIDE = "carbonmonoxide"; public static final String CHANNEL_AIRQUALITY = "airquality"; public static final String CHANNEL_AIRQUALITYPPB = "airqualityppb"; + public static final String CHANNEL_MOISTURE = "moisture"; public static final String CHANNEL_HEATSETPOINT = "heatsetpoint"; public static final String CHANNEL_THERMOSTAT_MODE = "mode"; + public static final String CHANNEL_THERMOSTAT_LOCKED = "locked"; public static final String CHANNEL_TEMPERATURE_OFFSET = "offset"; public static final String CHANNEL_VALVE_POSITION = "valve"; - public static final String CHANNEL_WINDOWOPEN = "windowopen"; + public static final String CHANNEL_WINDOW_OPEN = "windowopen"; + public static final String CHANNEL_EXTERNAL_WINDOW_OPEN = "externalwindowopen"; // group + light channel ids public static final String CHANNEL_SWITCH = "switch"; @@ -122,6 +129,11 @@ public class BindingConstants { public static final String CHANNEL_SCENE = "scene"; public static final String CHANNEL_ONTIME = "ontime"; + // channel uids + public static final ChannelTypeUID CHANNEL_EFFECT_TYPE_UID = new ChannelTypeUID(BINDING_ID, CHANNEL_EFFECT); + public static final ChannelTypeUID CHANNEL_EFFECT_SPEED_TYPE_UID = new ChannelTypeUID(BINDING_ID, + CHANNEL_EFFECT_SPEED); + // Thing configuration public static final String CONFIG_HOST = "host"; public static final String CONFIG_HTTP_PORT = "httpPort"; diff --git a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/DeconzDynamicCommandDescriptionProvider.java b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/DeconzDynamicCommandDescriptionProvider.java index 4bb7198ad..57a23dd04 100644 --- a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/DeconzDynamicCommandDescriptionProvider.java +++ b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/DeconzDynamicCommandDescriptionProvider.java @@ -13,10 +13,15 @@ package org.openhab.binding.deconz.internal; import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.events.EventPublisher; import org.openhab.core.thing.ThingUID; import org.openhab.core.thing.binding.BaseDynamicCommandDescriptionProvider; +import org.openhab.core.thing.i18n.ChannelTypeI18nLocalizationService; +import org.openhab.core.thing.link.ItemChannelLinkRegistry; import org.openhab.core.thing.type.DynamicCommandDescriptionProvider; +import org.osgi.service.component.annotations.Activate; import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -29,6 +34,16 @@ import org.slf4j.LoggerFactory; @NonNullByDefault @Component(service = { DynamicCommandDescriptionProvider.class, DeconzDynamicCommandDescriptionProvider.class }) public class DeconzDynamicCommandDescriptionProvider extends BaseDynamicCommandDescriptionProvider { + + @Activate + public DeconzDynamicCommandDescriptionProvider(final @Reference EventPublisher eventPublisher, // + final @Reference ItemChannelLinkRegistry itemChannelLinkRegistry, // + final @Reference ChannelTypeI18nLocalizationService channelTypeI18nLocalizationService) { + this.eventPublisher = eventPublisher; + this.itemChannelLinkRegistry = itemChannelLinkRegistry; + this.channelTypeI18nLocalizationService = channelTypeI18nLocalizationService; + } + private final Logger logger = LoggerFactory.getLogger(DeconzDynamicCommandDescriptionProvider.class); /** @@ -36,7 +51,7 @@ public class DeconzDynamicCommandDescriptionProvider extends BaseDynamicCommandD * * @param thingUID the thing's UID */ - public void removeDescriptionsForThing(ThingUID thingUID) { + public void removeCommandDescriptionForThing(ThingUID thingUID) { logger.trace("removing state description for thing {}", thingUID); channelOptionsMap.entrySet().removeIf(entry -> entry.getKey().getThingUID().equals(thingUID)); } diff --git a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/DeconzDynamicStateDescriptionProvider.java b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/DeconzDynamicStateDescriptionProvider.java index 77b892d59..4a388e51e 100644 --- a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/DeconzDynamicStateDescriptionProvider.java +++ b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/DeconzDynamicStateDescriptionProvider.java @@ -19,16 +19,20 @@ import java.util.concurrent.ConcurrentHashMap; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.events.EventPublisher; import org.openhab.core.thing.Channel; import org.openhab.core.thing.ChannelUID; import org.openhab.core.thing.ThingUID; import org.openhab.core.thing.binding.BaseDynamicStateDescriptionProvider; import org.openhab.core.thing.events.ThingEventFactory; +import org.openhab.core.thing.i18n.ChannelTypeI18nLocalizationService; import org.openhab.core.thing.link.ItemChannelLinkRegistry; import org.openhab.core.thing.type.DynamicStateDescriptionProvider; import org.openhab.core.types.StateDescription; import org.openhab.core.types.StateDescriptionFragment; +import org.osgi.service.component.annotations.Activate; import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -45,6 +49,15 @@ public class DeconzDynamicStateDescriptionProvider extends BaseDynamicStateDescr private final Map stateDescriptionFragments = new ConcurrentHashMap<>(); + @Activate + public DeconzDynamicStateDescriptionProvider(final @Reference EventPublisher eventPublisher, // + final @Reference ItemChannelLinkRegistry itemChannelLinkRegistry, // + final @Reference ChannelTypeI18nLocalizationService channelTypeI18nLocalizationService) { + this.eventPublisher = eventPublisher; + this.itemChannelLinkRegistry = itemChannelLinkRegistry; + this.channelTypeI18nLocalizationService = channelTypeI18nLocalizationService; + } + /** * Set a state description for a channel. This description will be used when preparing the channel state by * the framework for presentation. A previous description, if existed, will be replaced. @@ -59,9 +72,10 @@ public class DeconzDynamicStateDescriptionProvider extends BaseDynamicStateDescr if (!stateDescriptionFragment.equals(oldStateDescriptionFragment)) { logger.trace("adding state description for channel {}", channelUID); stateDescriptionFragments.put(channelUID, stateDescriptionFragment); - ItemChannelLinkRegistry itemChannelLinkRegistry = this.itemChannelLinkRegistry; + ItemChannelLinkRegistry localItemChannelLinkRegistry = itemChannelLinkRegistry; postEvent(ThingEventFactory.createChannelDescriptionChangedEvent(channelUID, - itemChannelLinkRegistry != null ? itemChannelLinkRegistry.getLinkedItemNames(channelUID) : Set.of(), + localItemChannelLinkRegistry != null ? localItemChannelLinkRegistry.getLinkedItemNames(channelUID) + : Set.of(), stateDescriptionFragment, oldStateDescriptionFragment)); } } 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 f41287b19..584d01409 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 @@ -19,9 +19,11 @@ import java.time.ZoneId; import java.time.ZoneOffset; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; +import java.util.Map; import java.util.stream.Collectors; import java.util.stream.Stream; +import org.eclipse.jdt.annotation.NonNull; import org.eclipse.jdt.annotation.NonNullByDefault; import org.openhab.core.library.types.DateTimeType; import org.openhab.core.library.types.PercentType; @@ -59,7 +61,7 @@ public class Util { } /** - * convert a brightness value from int to PercentType + * Convert a brightness value from int to PercentType * * @param val the value * @return the corresponding PercentType value @@ -67,11 +69,11 @@ public class Util { public static PercentType toPercentType(int val) { int scaledValue = (int) Math.ceil(val / BRIGHTNESS_FACTOR); return new PercentType( - Util.constrainToRange(scaledValue, PercentType.ZERO.intValue(), PercentType.HUNDRED.intValue())); + constrainToRange(scaledValue, PercentType.ZERO.intValue(), PercentType.HUNDRED.intValue())); } /** - * convert a brightness value from PercentType to int + * Convert a brightness value from PercentType to int * * @param val the value * @return the corresponding int value @@ -81,7 +83,7 @@ public class Util { } /** - * convert a timestamp string to a DateTimeType + * Convert a timestamp string to a DateTimeType * * @param timestamp either in zoned date time or local date time format * @return the corresponding DateTimeType @@ -95,4 +97,15 @@ public class Util { ZoneOffset.UTC, ZoneId.systemDefault())); } } + + /** + * Get all keys corresponding to a given value of a map + * + * @param map a map + * @param value the value to find in the map + * @return Stream of all keys for the value + */ + public static <@NonNull K, @NonNull V> Stream getKeysFromValue(Map map, V value) { + return map.entrySet().stream().filter(e -> e.getValue().equals(value)).map(Map.Entry::getKey); + } } diff --git a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/action/BridgeActions.java b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/action/BridgeActions.java new file mode 100644 index 000000000..ec8a1fd3a --- /dev/null +++ b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/action/BridgeActions.java @@ -0,0 +1,87 @@ +/** + * Copyright (c) 2010-2023 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.action; + +import java.util.Map; +import java.util.Objects; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.http.HttpMethod; +import org.openhab.binding.deconz.internal.Util; +import org.openhab.binding.deconz.internal.handler.DeconzBridgeHandler; +import org.openhab.core.automation.annotation.ActionInput; +import org.openhab.core.automation.annotation.RuleAction; +import org.openhab.core.thing.binding.ThingActions; +import org.openhab.core.thing.binding.ThingActionsScope; +import org.openhab.core.thing.binding.ThingHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link BridgeActions} provides actions for managing scenes in groups + * + * @author Jan N. Klug - Initial contribution + */ +@ThingActionsScope(name = "deconz") +@NonNullByDefault +public class BridgeActions implements ThingActions { + private final Logger logger = LoggerFactory.getLogger(BridgeActions.class); + + private @Nullable DeconzBridgeHandler handler; + + @RuleAction(label = "@text/action.permit-join-network.label", description = "@text/action.permit-join-network.description") + public void permitJoin( + @ActionInput(name = "duration", label = "@text/action.permit-join-network.duration.label", description = "@text/action.permit-join-network.duration.description") @Nullable Integer duration) { + DeconzBridgeHandler handler = this.handler; + + if (handler == null) { + logger.warn("Deconz BridgeActions service ThingHandler is null!"); + return; + } + + int searchDuration = Util.constrainToRange(Objects.requireNonNullElse(duration, 120), 1, 240); + + Object object = Map.of("permitjoin", searchDuration); + handler.sendObject("config", object, HttpMethod.PUT).thenAccept(v -> { + if (v.getResponseCode() != java.net.HttpURLConnection.HTTP_OK) { + logger.warn("Sending {} via PUT to config failed: {} - {}", object, v.getResponseCode(), v.getBody()); + } else { + logger.trace("Result code={}, body={}", v.getResponseCode(), v.getBody()); + logger.info("Enabled device searching for {} seconds on bridge {}.", searchDuration, + handler.getThing().getUID()); + } + }).exceptionally(e -> { + logger.warn("Sending {} via PUT to config failed: {} - {}", object, e.getClass(), e.getMessage()); + return null; + }); + } + + public static void permitJoin(ThingActions actions, @Nullable Integer duration) { + if (actions instanceof BridgeActions bridgeActions) { + bridgeActions.permitJoin(duration); + } + } + + @Override + public void setThingHandler(@Nullable ThingHandler handler) { + if (handler instanceof DeconzBridgeHandler) { + this.handler = (DeconzBridgeHandler) handler; + } + } + + @Override + public @Nullable ThingHandler getThingHandler() { + return handler; + } +} diff --git a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/action/GroupActions.java b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/action/GroupActions.java new file mode 100644 index 000000000..79f005781 --- /dev/null +++ b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/action/GroupActions.java @@ -0,0 +1,156 @@ +/** + * Copyright (c) 2010-2023 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.action; + +import java.lang.reflect.Type; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.http.HttpMethod; +import org.openhab.binding.deconz.internal.dto.NewSceneResponse; +import org.openhab.binding.deconz.internal.handler.GroupThingHandler; +import org.openhab.core.automation.annotation.ActionInput; +import org.openhab.core.automation.annotation.ActionOutput; +import org.openhab.core.automation.annotation.RuleAction; +import org.openhab.core.thing.binding.ThingActions; +import org.openhab.core.thing.binding.ThingActionsScope; +import org.openhab.core.thing.binding.ThingHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.gson.Gson; +import com.google.gson.JsonParseException; +import com.google.gson.reflect.TypeToken; + +/** + * The {@link GroupActions} provides actions for managing scenes in groups + * + * @author Jan N. Klug - Initial contribution + */ +@ThingActionsScope(name = "deconz") +@NonNullByDefault +public class GroupActions implements ThingActions { + private static final String NEW_SCENE_ID_OUTPUT = "newSceneId"; + private static final Type NEW_SCENE_RESPONSE_TYPE = new TypeToken>() { + }.getType(); + + private final Logger logger = LoggerFactory.getLogger(GroupActions.class); + private final Gson gson = new Gson(); + + private @Nullable GroupThingHandler handler; + + @RuleAction(label = "@text/action.create-scene.label", description = "@text/action.create-scene.description") + public @ActionOutput(name = NEW_SCENE_ID_OUTPUT, type = "java.lang.Integer") Map createScene( + @ActionInput(name = "name", label = "@text/action.create-scene.name.label", description = "@text/action.create-scene.name.description") @Nullable String name) { + GroupThingHandler handler = this.handler; + + if (handler == null) { + logger.warn("Deconz GroupActions service ThingHandler is null!"); + return Map.of(); + } + + if (name == null) { + logger.debug("Skipping scene creation due to missing scene name"); + return Map.of(); + } + + CompletableFuture newSceneId = new CompletableFuture<>(); + handler.doNetwork(Map.of("name", name), "scenes", HttpMethod.POST, newSceneId::complete); + + try { + String returnedJson = newSceneId.get(2000, TimeUnit.MILLISECONDS); + List newSceneResponses = gson.fromJson(returnedJson, NEW_SCENE_RESPONSE_TYPE); + if (newSceneResponses != null && !newSceneResponses.isEmpty()) { + return Map.of(NEW_SCENE_ID_OUTPUT, newSceneResponses.get(0).success.id); + } + throw new IllegalStateException("response is empty"); + } catch (InterruptedException | ExecutionException | TimeoutException | JsonParseException + | IllegalStateException e) { + logger.warn("Couldn't get newSceneId", e); + return Map.of(); + } + } + + public static Map createScene(ThingActions actions, @Nullable String name) { + if (actions instanceof GroupActions groupActions) { + return groupActions.createScene(name); + } + return Map.of(); + } + + @RuleAction(label = "@text/action.delete-scene.label", description = "@text/action.delete-scene.description") + public void deleteScene( + @ActionInput(name = "sceneId", label = "@text/action.delete-scene.sceneId.label", description = "@text/action.delete-scene.sceneId.description") @Nullable Integer sceneId) { + GroupThingHandler handler = this.handler; + + if (handler == null) { + logger.warn("Deconz GroupActions service ThingHandler is null!"); + return; + } + + if (sceneId == null) { + logger.warn("Skipping scene deletion due to missing scene id"); + return; + } + + handler.doNetwork(null, "scenes/" + sceneId, HttpMethod.DELETE, null); + } + + public static void deleteScene(ThingActions actions, @Nullable Integer sceneId) { + if (actions instanceof GroupActions groupActions) { + groupActions.deleteScene(sceneId); + } + } + + @RuleAction(label = "@text/action.store-as-scene.label", description = "@text/action.store-as-scene.description") + public void storeScene( + @ActionInput(name = "sceneId", label = "@text/action.store-as-scene.sceneId.label", description = "@text/action.store-as-scene.sceneId.description") @Nullable Integer sceneId) { + GroupThingHandler handler = this.handler; + + if (handler == null) { + logger.warn("Deconz GroupActions service ThingHandler is null!"); + return; + } + + if (sceneId == null) { + logger.warn("Skipping scene storage due to missing scene id"); + return; + } + + handler.doNetwork(null, "scenes/" + sceneId + "/store", HttpMethod.PUT, null); + } + + public static void storeScene(ThingActions actions, @Nullable Integer sceneId) { + if (actions instanceof GroupActions groupActions) { + groupActions.storeScene(sceneId); + } + } + + @Override + public void setThingHandler(@Nullable ThingHandler handler) { + if (handler instanceof GroupThingHandler) { + this.handler = (GroupThingHandler) handler; + } + } + + @Override + public @Nullable ThingHandler getThingHandler() { + return handler; + } +} diff --git a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/discovery/BridgeDiscoveryParticipant.java b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/discovery/BridgeDiscoveryParticipant.java index 929469360..74894aafa 100644 --- a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/discovery/BridgeDiscoveryParticipant.java +++ b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/discovery/BridgeDiscoveryParticipant.java @@ -58,7 +58,7 @@ public class BridgeDiscoveryParticipant implements UpnpDiscoveryParticipant { return null; } URL descriptorURL = device.getIdentity().getDescriptorURL(); - String UDN = device.getIdentity().getUdn().getIdentifierString(); + String udn = device.getIdentity().getUdn().getIdentifierString(); // Friendly name is like "name (host)" String name = device.getDetails().getFriendlyName(); @@ -75,7 +75,7 @@ public class BridgeDiscoveryParticipant implements UpnpDiscoveryParticipant { properties.put(CONFIG_HOST, host); properties.put(CONFIG_HTTP_PORT, port); - properties.put(PROPERTY_UDN, UDN); + properties.put(PROPERTY_UDN, udn); return DiscoveryResultBuilder.create(uid).withProperties(properties).withLabel(name) .withRepresentationProperty(PROPERTY_UDN).build(); 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 fafcbbbbf..0c391c44f 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 @@ -14,6 +14,7 @@ package org.openhab.binding.deconz.internal.discovery; import static org.openhab.binding.deconz.internal.BindingConstants.*; +import java.util.Date; import java.util.HashMap; import java.util.Map; import java.util.Set; @@ -74,7 +75,6 @@ public class ThingDiscoveryService extends AbstractDiscoveryService implements D if (handler != null) { handler.getBridgeFullState().thenAccept(fullState -> { stopScan(); - removeOlderResults(getTimestampOfLastScan()); fullState.ifPresent(state -> { state.sensors.forEach(this::addSensor); state.lights.forEach(this::addLight); @@ -85,6 +85,12 @@ public class ThingDiscoveryService extends AbstractDiscoveryService implements D } } + @Override + protected synchronized void stopScan() { + removeOlderResults(getTimestampOfLastScan()); + super.stopScan(); + } + @Override protected void startBackgroundDiscovery() { final ScheduledFuture scanningJob = this.scanningJob; @@ -127,14 +133,17 @@ public class ThingDiscoveryService extends AbstractDiscoveryService implements D properties.put(CONFIG_ID, groupId); switch (groupType) { - case LIGHT_GROUP: - thingTypeUID = THING_TYPE_LIGHTGROUP; - break; - default: + case LIGHT_GROUP -> thingTypeUID = THING_TYPE_LIGHTGROUP; + case LUMINAIRE, LIGHT_SOURCE, ROOM -> { + logger.debug("Group {} ({}), type {} ignored.", group.id, group.name, group.type); + return; + } + 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); @@ -179,42 +188,24 @@ public class ThingDiscoveryService extends AbstractDiscoveryService implements D } switch (lightType) { - case ON_OFF_LIGHT: - case ON_OFF_PLUGIN_UNIT: - case SMART_PLUG: - thingTypeUID = THING_TYPE_ONOFF_LIGHT; - break; - case DIMMABLE_LIGHT: - case DIMMABLE_PLUGIN_UNIT: - thingTypeUID = THING_TYPE_DIMMABLE_LIGHT; - break; - case COLOR_TEMPERATURE_LIGHT: - thingTypeUID = THING_TYPE_COLOR_TEMPERATURE_LIGHT; - break; - case COLOR_DIMMABLE_LIGHT: - case COLOR_LIGHT: - thingTypeUID = THING_TYPE_COLOR_LIGHT; - break; - case EXTENDED_COLOR_LIGHT: - thingTypeUID = THING_TYPE_EXTENDED_COLOR_LIGHT; - break; - case WINDOW_COVERING_DEVICE: - thingTypeUID = THING_TYPE_WINDOW_COVERING; - break; - case WARNING_DEVICE: - thingTypeUID = THING_TYPE_WARNING_DEVICE; - break; - case DOORLOCK: - thingTypeUID = THING_TYPE_DOORLOCK; - break; - case CONFIGURATION_TOOL: + case ON_OFF_LIGHT, ON_OFF_PLUGIN_UNIT, SMART_PLUG -> thingTypeUID = THING_TYPE_ONOFF_LIGHT; + case DIMMABLE_LIGHT, DIMMABLE_PLUGIN_UNIT -> thingTypeUID = THING_TYPE_DIMMABLE_LIGHT; + case COLOR_TEMPERATURE_LIGHT -> thingTypeUID = THING_TYPE_COLOR_TEMPERATURE_LIGHT; + case COLOR_DIMMABLE_LIGHT, COLOR_LIGHT -> thingTypeUID = THING_TYPE_COLOR_LIGHT; + case EXTENDED_COLOR_LIGHT -> thingTypeUID = THING_TYPE_EXTENDED_COLOR_LIGHT; + case WINDOW_COVERING_DEVICE -> thingTypeUID = THING_TYPE_WINDOW_COVERING; + case WARNING_DEVICE -> thingTypeUID = THING_TYPE_WARNING_DEVICE; + case DOORLOCK -> thingTypeUID = THING_TYPE_DOORLOCK; + case CONFIGURATION_TOOL -> { // ignore configuration tool device return; - default: + } + default -> { logger.debug( "Found light: {} ({}), type {} but no thing type defined for that type. This should be reported.", light.modelid, light.name, light.type); return; + } } ThingUID uid = new ThingUID(thingTypeUID, bridgeUID, light.uniqueid.replaceAll("[^a-z0-9\\[\\]]", "")); @@ -261,6 +252,8 @@ public class ThingDiscoveryService extends AbstractDiscoveryService implements D } } else if (sensor.type.contains("LightLevel")) { // ZHALightLevel thingTypeUID = THING_TYPE_LIGHT_SENSOR; + } else if (sensor.type.contains("ZHAAirQuality")) { // ZHAAirQuality + thingTypeUID = THING_TYPE_AIRQUALITY_SENSOR; } else if (sensor.type.contains("ZHATemperature")) { // ZHATemperature thingTypeUID = THING_TYPE_TEMPERATURE_SENSOR; } else if (sensor.type.contains("ZHAHumidity")) { // ZHAHumidity @@ -279,10 +272,10 @@ public class ThingDiscoveryService extends AbstractDiscoveryService implements D thingTypeUID = THING_TYPE_VIBRATION_SENSOR; // ZHAVibration } else if (sensor.type.contains("ZHABattery")) { thingTypeUID = THING_TYPE_BATTERY_SENSOR; // ZHABattery + } else if (sensor.type.contains("ZHAMoisture")) { + thingTypeUID = THING_TYPE_MOISTURE_SENSOR; // ZHAMoisture } else if (sensor.type.contains("ZHAThermostat")) { thingTypeUID = THING_TYPE_THERMOSTAT; // ZHAThermostat - } else if (sensor.type.contains("ZHAAirQuality")) { - thingTypeUID = THING_TYPE_AIRQUALITY_SENSOR; } else { logger.debug("Unknown type {}", sensor.type); return; @@ -316,6 +309,7 @@ public class ThingDiscoveryService extends AbstractDiscoveryService implements D @Override public void deactivate() { + removeOlderResults(new Date().getTime()); super.deactivate(); } } 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 bdd2019e7..503a78614 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 @@ -25,11 +25,15 @@ import org.openhab.binding.deconz.internal.types.ResourceType; @NonNullByDefault public class DeconzBaseMessage { // For websocket change events - public String e = ""; // "changed" + public String e = ""; // "changed", "scene-called" public ResourceType r = ResourceType.UNKNOWN; // "sensors" public String t = ""; // "event" public String id = ""; // "3" + // for scene-recall + public String gid = ""; + public String scid = ""; + // for rest API public String manufacturername = ""; public String modelid = ""; 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 index 2abb18363..9968d2c59 100644 --- 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 @@ -38,6 +38,23 @@ public class GroupAction { public @Nullable Integer colorloopspeed; public @Nullable Integer transitiontime; + /** + * clear this group action + */ + public void clear() { + on = null; + bri = null; + + alert = null; + colormode = null; + effect = null; + + hue = null; + sat = null; + ct = null; + xy = null; + } + @Override public String toString() { return "GroupAction{on=" + on + ", toggle=" + toggle + ", bri=" + bri + ", hue=" + hue + ", sat=" + sat 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 index fae765d84..2cbcc4035 100644 --- 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 @@ -14,6 +14,8 @@ package org.openhab.binding.deconz.internal.dto; import org.eclipse.jdt.annotation.NonNullByDefault; +import com.google.gson.annotations.SerializedName; + /** * The {@link GroupState} is send by the websocket connection as well as the Rest API. * It is part of a {@link GroupMessage}. @@ -22,11 +24,13 @@ import org.eclipse.jdt.annotation.NonNullByDefault; */ @NonNullByDefault public class GroupState { - public boolean all_on; - public boolean any_on; + @SerializedName(value = "all_on") + public boolean allOn; + @SerializedName(value = "any_on") + public boolean anyOn; @Override public String toString() { - return "GroupState{" + "all_on=" + all_on + ", any_on=" + any_on + '}'; + return "GroupState{" + "all_on=" + allOn + ", any_on=" + anyOn + '}'; } } diff --git a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/dto/LightState.java b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/dto/LightState.java index 88e55ad84..8622acfc9 100644 --- a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/dto/LightState.java +++ b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/dto/LightState.java @@ -44,6 +44,11 @@ public class LightState { public @Nullable Integer ct; public double @Nullable [] xy; + // for window covering + public @Nullable Boolean open; + public @Nullable Boolean stop; + public @Nullable Integer lift; + public @Nullable Integer transitiontime; /** diff --git a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/dto/NewSceneResponse.java b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/dto/NewSceneResponse.java new file mode 100644 index 000000000..321598895 --- /dev/null +++ b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/dto/NewSceneResponse.java @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2010-2023 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 NewSceneResponse} is the response after a successful scene creation + * + * @author Jan N. Klug - Initial contribution + */ +@NonNullByDefault +public class NewSceneResponse { + public Success success = new Success(); + + public static class Success { + public int id = 0; + } +} diff --git a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/dto/SensorConfig.java b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/dto/SensorConfig.java index d46ae5b3b..84355cf0c 100644 --- a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/dto/SensorConfig.java +++ b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/dto/SensorConfig.java @@ -35,10 +35,13 @@ public class SensorConfig { public @Nullable Integer heatsetpoint; public @Nullable ThermostatMode mode; public @Nullable Integer offset; + public @Nullable Boolean locked; + public @Nullable Boolean externalwindowopen; @Override public String toString() { return "SensorConfig{" + "on=" + on + ", reachable=" + reachable + ", battery=" + battery + ", temperature=" - + temperature + ", heatsetpoint=" + heatsetpoint + ", mode=" + mode + ", offset=" + offset + "}"; + + temperature + ", heatsetpoint=" + heatsetpoint + ", mode=" + mode + ", offset=" + offset + ", locked=" + + locked + ", externalwindowopen=" + externalwindowopen + "}"; } } diff --git a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/dto/SensorState.java b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/dto/SensorState.java index 69634d329..2d38f8c0b 100644 --- a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/dto/SensorState.java +++ b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/dto/SensorState.java @@ -37,9 +37,9 @@ public class SensorState { /** Light sensors provide a lux value. */ public @Nullable Integer lux; /** Temperature sensors provide a degrees value. */ - public @Nullable Float temperature; + public @Nullable Double temperature; /** Humidity sensors provide a percent value. */ - public @Nullable Float humidity; + public @Nullable Double humidity; /** OpenClose sensors provide a boolean value. */ public @Nullable Boolean open; /** fire sensors provide a boolean value. */ @@ -54,29 +54,23 @@ public class SensorState { public @Nullable Boolean vibration; /** carbonmonoxide sensors provide a boolean value. */ public @Nullable Boolean carbonmonoxide; - /** airquality sensors provide a string value. */ - public @Nullable String airquality; - /** airquality sensors provide an integer value. */ - public @Nullable Integer airqualityppb; /** Pressure sensors provide a hPa value. */ public @Nullable Integer pressure; /** Presence sensors provide this boolean. */ public @Nullable Boolean presence; /** Power sensors provide this value in Watts. */ - public @Nullable Float power; + public @Nullable Double power; /** Batttery sensors provide this value */ public @Nullable Integer battery; - /** - * Some battery sensors (especially Tuya driven devices) provide this boolean - * instead of battery level - */ + /** Consumption sensors provide this value in Watts/hour. */ public @Nullable Boolean lowbattery; /** Consumption sensors provide this value in Watts/hour. */ - public @Nullable Float consumption; + public @Nullable Double consumption; + public @Nullable Double consumption2; /** Power sensors provide this value in Volt. */ - public @Nullable Float voltage; + public @Nullable Double voltage; /** Power sensors provide this value in Milliampere. */ - public @Nullable Float current; + public @Nullable Double current; /** Light sensors and the daylight sensor provide a status integer that can have various semantics. */ public @Nullable Integer status; /** Switches provide this value. */ @@ -85,6 +79,11 @@ public class SensorState { public @Nullable Integer gesture; /** Thermostat may provide this value. */ public @Nullable Integer valve; + /** air quality sensors provide this value */ + public @Nullable String airquality; + public @Nullable Integer airqualityppb; + /** moisture sensors provide this value */ + public @Nullable Integer moisture; /** Thermostats may provide this value */ public @Nullable String windowopen; /** deCONZ sends a last update string with every event. */ @@ -97,11 +96,11 @@ public class SensorState { return "SensorState{" + "dark=" + dark + ", daylight=" + daylight + ", lightlevel=" + lightlevel + ", lux=" + lux + ", temperature=" + temperature + ", humidity=" + humidity + ", open=" + open + ", fire=" + fire + ", water=" + water + ", alarm=" + alarm + ", tampered=" + tampered + ", vibration=" + vibration - + ", carbonmonoxide=" + carbonmonoxide + ", airquality=" + airquality + ", airqualityppb=" - + airqualityppb + ", pressure=" + pressure + ", presence=" + presence + ", power=" + power - + ", battery=" + battery + ", consumption=" + consumption + ", voltage=" + voltage + ", current=" - + current + ", status=" + status + ", buttonevent=" + buttonevent + ", gesture=" + gesture + ", valve=" - + valve + ", windowopen='" + windowopen + '\'' + ", lastupdated='" + lastupdated + '\'' + ", xy=" - + Arrays.toString(xy) + '}'; + + ", carbonmonoxide=" + carbonmonoxide + ", pressure=" + pressure + ", presence=" + presence + + ", power=" + power + ", battery=" + battery + ", lowbattery=" + lowbattery + ", consumption=" + + consumption + ", voltage=" + voltage + ", current=" + current + ", status=" + status + + ", buttonevent=" + buttonevent + ", gesture=" + gesture + ", valve=" + valve + ", airquality='" + + airquality + "'" + ", airqualityppb=" + airqualityppb + ", windowopen='" + windowopen + "'" + + ", lastupdated='" + lastupdated + "'" + ", xy=" + Arrays.toString(xy) + "}"; } } diff --git a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/dto/ThermostatUpdateConfig.java b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/dto/ThermostatUpdateConfig.java index cbce35122..4b7a36663 100644 --- a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/dto/ThermostatUpdateConfig.java +++ b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/dto/ThermostatUpdateConfig.java @@ -26,4 +26,6 @@ public class ThermostatUpdateConfig { public @Nullable Integer heatsetpoint; public @Nullable ThermostatMode mode; public @Nullable Integer offset; + public @Nullable Boolean locked; + public @Nullable Boolean externalwindowopen; } 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 8717620c7..7ae7a6cfa 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 @@ -13,19 +13,26 @@ package org.openhab.binding.deconz.internal.handler; import static org.openhab.binding.deconz.internal.BindingConstants.*; +import static org.openhab.binding.deconz.internal.Util.toPercentType; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import java.util.function.Consumer; -import java.util.stream.Collectors; -import java.util.stream.Stream; + +import javax.measure.Unit; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.http.HttpMethod; +import org.openhab.binding.deconz.internal.Util; import org.openhab.binding.deconz.internal.dto.DeconzBaseMessage; import org.openhab.binding.deconz.internal.netutils.WebSocketConnection; import org.openhab.binding.deconz.internal.netutils.WebSocketMessageListener; 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; +import org.openhab.core.library.types.StringType; import org.openhab.core.thing.Bridge; import org.openhab.core.thing.Channel; import org.openhab.core.thing.ChannelUID; @@ -35,6 +42,7 @@ import org.openhab.core.thing.ThingStatusDetail; import org.openhab.core.thing.ThingStatusInfo; import org.openhab.core.thing.binding.BaseThingHandler; import org.openhab.core.thing.binding.ThingHandlerCallback; +import org.openhab.core.thing.binding.builder.ThingBuilder; import org.openhab.core.thing.type.ChannelKind; import org.openhab.core.thing.type.ChannelTypeUID; import org.openhab.core.types.Command; @@ -58,7 +66,9 @@ public abstract class DeconzBaseThingHandler extends BaseThingHandler implements protected final ResourceType resourceType; protected ThingConfig config = new ThingConfig(); protected final Gson gson; + private @Nullable ScheduledFuture initializationJob; + private @Nullable ScheduledFuture lastSeenPollingJob; protected @Nullable WebSocketConnection connection; public DeconzBaseThingHandler(Thing thing, Gson gson, ResourceType resourceType) { @@ -68,7 +78,7 @@ public abstract class DeconzBaseThingHandler extends BaseThingHandler implements } /** - * Stops the API request + * Stops the initialization request */ private void stopInitializationJob() { ScheduledFuture future = initializationJob; @@ -78,10 +88,14 @@ public abstract class DeconzBaseThingHandler extends BaseThingHandler implements } } - private void registerListener() { - WebSocketConnection conn = connection; - if (conn != null) { - conn.registerListener(resourceType, config.id, this); + /** + * Stops the last_seen polling + */ + private void stopLastSeenPollingJob() { + ScheduledFuture future = lastSeenPollingJob; + if (future != null) { + future.cancel(true); + lastSeenPollingJob = null; } } @@ -117,13 +131,12 @@ public abstract class DeconzBaseThingHandler extends BaseThingHandler implements return; } - final WebSocketConnection webSocketConnection = bridgeHandler.getWebsocketConnection(); - this.connection = webSocketConnection; - updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.NONE); // Real-time data - registerListener(); + WebSocketConnection socketConnection = bridgeHandler.getWebSocketConnection(); + this.connection = socketConnection; + socketConnection.registerListener(resourceType, config.id, this); // get initial values requestState(this::processStateResponse); @@ -145,7 +158,7 @@ public abstract class DeconzBaseThingHandler extends BaseThingHandler implements protected abstract void processStateResponse(DeconzBaseMessage stateResponse); /** - * Perform a request to the REST API for retrieving the full light state with all data and configuration. + * Perform a request to the REST API for retrieving the full state with all data and configuration. */ protected void requestState(Consumer processor) { DeconzBridgeHandler bridgeHandler = getBridgeHandler(); @@ -164,12 +177,84 @@ public abstract class DeconzBaseThingHandler extends BaseThingHandler implements } } + /** + * create a channel on the current thing + * + * @param thingBuilder a ThingBuilder instance for this thing + * @param channelId the channel id + * @param kind the channel kind (STATE or TRIGGER) + * @return true if the thing was modified + */ + protected boolean createChannel(ThingBuilder thingBuilder, String channelId, ChannelKind kind) { + if (thing.getChannel(channelId) != null) { + // channel already exists, no update necessary + return false; + } + + ChannelUID channelUID = new ChannelUID(thing.getUID(), channelId); + ChannelTypeUID channelTypeUID = switch (channelId) { + case CHANNEL_BATTERY_LEVEL -> new ChannelTypeUID("system:battery-level"); + case CHANNEL_BATTERY_LOW -> new ChannelTypeUID("system:low-battery"); + case CHANNEL_CONSUMPTION_2 -> new ChannelTypeUID("deconz:consumption"); + default -> new ChannelTypeUID(BINDING_ID, channelId); + }; + + ThingHandlerCallback callback = getCallback(); + if (callback != null) { + Channel channel = callback.createChannelBuilder(channelUID, channelTypeUID).withKind(kind).build(); + thingBuilder.withChannel(channel); + logger.trace("Added '{}' to thing '{}'", channelId, thing.getUID()); + + return true; + } + + logger.warn("Could not create channel '{}' for thing '{}'", channelUID, thing.getUID()); + return false; + } + + /** + * check if we need to add a last seen channel (called from processStateResponse only) + * + * @param thingBuilder a ThingBuilder instance for this thing + * @param lastSeen the lastSeen string of a deconz message + * @return true if the thing was modified + */ + protected boolean checkLastSeen(ThingBuilder thingBuilder, @Nullable String lastSeen) { + // "Last seen" is the last "ping" from the device, whereas "last update" is the last status changed. + // For example, for a fire sensor, the device pings regularly, without necessarily updating channels. + // So to monitor a sensor is still alive, the "last seen" is necessary. + // Because "last seen" is never updated by the WebSocket API we have to + // manually poll it after the defined time if supported by the device + stopLastSeenPollingJob(); + boolean thingEdited = false; + if (lastSeen != null && config.lastSeenPolling > 0) { + thingEdited = createChannel(thingBuilder, CHANNEL_LAST_SEEN, ChannelKind.STATE); + updateState(CHANNEL_LAST_SEEN, Util.convertTimestampToDateTime(lastSeen)); + lastSeenPollingJob = scheduler.scheduleWithFixedDelay(() -> requestState(this::processLastSeen), + config.lastSeenPolling, config.lastSeenPolling, TimeUnit.MINUTES); + logger.trace("lastSeen polling enabled for thing {} with interval of {} minutes", thing.getUID(), + config.lastSeenPolling); + } else if (thing.getChannel(CHANNEL_LAST_SEEN) != null) { + thingBuilder.withoutChannel(new ChannelUID(thing.getUID(), CHANNEL_LAST_SEEN)); + thingEdited = true; + } + + return thingEdited; + } + + private void processLastSeen(DeconzBaseMessage stateResponse) { + String lastSeen = stateResponse.lastseen; + if (lastSeen != null) { + updateState(CHANNEL_LAST_SEEN, Util.convertTimestampToDateTime(lastSeen)); + } + } + /** * sends a command to the bridge with the default command URL * * @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 channelUID the channel that this command was sent to (used for logging purposes) * @param acceptProcessing additional processing after the command was successfully send (might be null) */ protected void sendCommand(@Nullable Object object, Command originalCommand, ChannelUID channelUID, @@ -182,7 +267,7 @@ public abstract class DeconzBaseThingHandler extends BaseThingHandler implements * * @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 channelUID the channel that this command was sent to (used for logging purposes) * @param commandUrl the command URL * @param acceptProcessing additional processing after the command was successfully send (might be null) */ @@ -192,10 +277,9 @@ public abstract class DeconzBaseThingHandler extends BaseThingHandler implements if (bridgeHandler == null) { return; } - String endpoint = Stream.of(resourceType.getIdentifier(), config.id, commandUrl) - .collect(Collectors.joining("/")); + String endpoint = String.join("/", resourceType.getIdentifier(), config.id, commandUrl); - bridgeHandler.sendObject(endpoint, object).thenAccept(v -> { + bridgeHandler.sendObject(endpoint, object, HttpMethod.PUT).thenAccept(v -> { if (acceptProcessing != null) { acceptProcessing.run(); } @@ -212,9 +296,35 @@ public abstract class DeconzBaseThingHandler extends BaseThingHandler implements }); } + public void doNetwork(@Nullable Object object, String commandUrl, HttpMethod httpMethod, + @Nullable Consumer acceptProcessing) { + DeconzBridgeHandler bridgeHandler = getBridgeHandler(); + if (bridgeHandler == null) { + return; + } + String endpoint = String.join("/", resourceType.getIdentifier(), config.id, commandUrl); + + bridgeHandler.sendObject(endpoint, object, httpMethod).thenAccept(v -> { + if (v.getResponseCode() != java.net.HttpURLConnection.HTTP_OK) { + logger.warn("Sending {} via {} to {} failed: {} - {}", object, httpMethod, commandUrl, + v.getResponseCode(), v.getBody()); + } else { + logger.trace("Result code={}, body={}", v.getResponseCode(), v.getBody()); + if (acceptProcessing != null) { + acceptProcessing.accept(v.getBody()); + } + } + }).exceptionally(e -> { + logger.warn("Sending {} via {} to {} failed: {} - {}", object, httpMethod, commandUrl, e.getClass(), + e.getMessage()); + return null; + }); + } + @Override public void dispose() { stopInitializationJob(); + stopLastSeenPollingJob(); unregisterListener(); super.dispose(); } @@ -229,29 +339,55 @@ public abstract class DeconzBaseThingHandler extends BaseThingHandler implements } } - protected void createChannel(String channelId, ChannelKind kind) { - if (thing.getChannel(channelId) != null) { - // channel already exists, no update necessary + protected void updateStringChannel(ChannelUID channelUID, @Nullable String value) { + if (value == null) { return; } + updateState(channelUID, new StringType(value)); + } - ThingHandlerCallback callback = getCallback(); - if (callback != null) { - ChannelUID channelUID = new ChannelUID(thing.getUID(), channelId); - ChannelTypeUID channelTypeUID; - switch (channelId) { - case CHANNEL_BATTERY_LEVEL: - channelTypeUID = new ChannelTypeUID("system:battery-level"); - break; - case CHANNEL_BATTERY_LOW: - channelTypeUID = new ChannelTypeUID("system:low-battery"); - break; - default: - channelTypeUID = new ChannelTypeUID(BINDING_ID, channelId); - break; - } - Channel channel = callback.createChannelBuilder(channelUID, channelTypeUID).withKind(kind).build(); - updateThing(editThing().withChannel(channel).build()); + protected void updateSwitchChannel(ChannelUID channelUID, @Nullable Boolean value) { + if (value == null) { + return; + } + updateState(channelUID, OnOffType.from(value)); + } + + protected void updateDecimalTypeChannel(ChannelUID channelUID, @Nullable Number value) { + if (value == null) { + return; + } + updateState(channelUID, new DecimalType(value.longValue())); + } + + protected void updateQuantityTypeChannel(ChannelUID channelUID, @Nullable Number value, Unit unit) { + updateQuantityTypeChannel(channelUID, value, unit, 1.0); + } + + protected void updateQuantityTypeChannel(ChannelUID channelUID, @Nullable Number value, Unit unit, + double scaling) { + if (value == null) { + return; + } + updateState(channelUID, new QuantityType<>(value.doubleValue() * scaling, unit)); + } + + /** + * Update a channel with a {@link org.openhab.core.library.types.PercentType} of {@link OnOffType} + * + * If either {@param value} or {@param on} are null or {@param on} is false the method + * updated the channel with {@link OnOffType#OFF}, otherwise {@param value} is scaled and converted to + * {@link org.openhab.core.library.types.PercentType} before updating the channel. + * + * @param channelUID the {@link ChannelUID} that shall receive the update + * @param value an {@link Integer} value (0-255) that is posted + * @param on the on state of the channel + */ + protected void updatePercentTypeChannel(ChannelUID channelUID, @Nullable Integer value, @Nullable Boolean on) { + if (value != null && on != null && on) { + updateState(channelUID, toPercentType(value)); + } else { + updateState(channelUID, OnOffType.OFF); } } } diff --git a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/handler/DeconzBridgeConfig.java b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/handler/DeconzBridgeConfig.java index bb8394812..dd375bd1d 100644 --- a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/handler/DeconzBridgeConfig.java +++ b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/handler/DeconzBridgeConfig.java @@ -26,7 +26,8 @@ public class DeconzBridgeConfig { public int httpPort = 80; public int port = 0; public @Nullable String apikey; - int timeout = 2000; + public int timeout = 2000; + public int websocketTimeout = 120; public String getHostWithoutPort() { String hostWithoutPort = host; diff --git a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/handler/DeconzBridgeHandler.java b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/handler/DeconzBridgeHandler.java index e5369a30f..a81b107f4 100644 --- a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/handler/DeconzBridgeHandler.java +++ b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/handler/DeconzBridgeHandler.java @@ -17,7 +17,6 @@ import static org.openhab.binding.deconz.internal.Util.buildUrl; import java.net.SocketTimeoutException; import java.util.Collection; -import java.util.Collections; import java.util.Map; import java.util.Objects; import java.util.Optional; @@ -30,6 +29,8 @@ import java.util.concurrent.TimeoutException; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.http.HttpMethod; +import org.openhab.binding.deconz.internal.action.BridgeActions; import org.openhab.binding.deconz.internal.discovery.ThingDiscoveryService; import org.openhab.binding.deconz.internal.dto.ApiKeyMessage; import org.openhab.binding.deconz.internal.dto.BridgeFullState; @@ -41,6 +42,7 @@ import org.openhab.core.config.core.Configuration; import org.openhab.core.io.net.http.WebSocketFactory; import org.openhab.core.thing.Bridge; import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; import org.openhab.core.thing.ThingStatus; import org.openhab.core.thing.ThingStatusDetail; import org.openhab.core.thing.ThingTypeUID; @@ -65,35 +67,43 @@ import com.google.gson.Gson; */ @NonNullByDefault public class DeconzBridgeHandler extends BaseBridgeHandler implements WebSocketConnectionListener { - public static final Set SUPPORTED_THING_TYPES = Collections.singleton(BRIDGE_TYPE); + public static final Set SUPPORTED_THING_TYPES = Set.of(BRIDGE_TYPE); private final Logger logger = LoggerFactory.getLogger(DeconzBridgeHandler.class); - private final WebSocketConnection websocket; private final AsyncHttpClient http; + private final WebSocketFactory webSocketFactory; private DeconzBridgeConfig config = new DeconzBridgeConfig(); private final Gson gson; - private @Nullable ScheduledFuture scheduledFuture; + private @Nullable ScheduledFuture connectionJob; private int websocketPort = 0; /** Prevent a dispose/init cycle while this flag is set. Use for property updates */ private boolean ignoreConfigurationUpdate; private boolean thingDisposing = false; + private WebSocketConnection webSocketConnection; private final ExpiringCacheAsync> fullStateCache = new ExpiringCacheAsync<>(1000); /** The poll frequency for the API Key verification */ private static final int POLL_FREQUENCY_SEC = 10; + private boolean ignoreConnectionLost = true; public DeconzBridgeHandler(Bridge thing, WebSocketFactory webSocketFactory, AsyncHttpClient http, Gson gson) { super(thing); this.http = http; this.gson = gson; + this.webSocketFactory = webSocketFactory; + this.webSocketConnection = createNewWebSocketConnection(); + } + + private WebSocketConnection createNewWebSocketConnection() { String websocketID = ThingWebClientUtil.buildWebClientConsumerName(thing.getUID(), null); - this.websocket = new WebSocketConnection(this, webSocketFactory.createWebSocketClient(websocketID), gson); + return new WebSocketConnection(this, webSocketFactory.createWebSocketClient(websocketID), gson, + config.websocketTimeout); } @Override public Collection> getServices() { - return Set.of(ThingDiscoveryService.class); + return Set.of(ThingDiscoveryService.class, BridgeActions.class); } @Override @@ -107,14 +117,23 @@ public class DeconzBridgeHandler extends BaseBridgeHandler implements WebSocketC public void handleCommand(ChannelUID channelUID, Command command) { } + @Override + public void thingUpdated(Thing thing) { + dispose(); + this.thing = thing; + // we need to create a new websocket connection, because it can't be restarted + webSocketConnection = createNewWebSocketConnection(); + initialize(); + } + /** * Stops the API request or websocket reconnect timer */ private void stopTimer() { - ScheduledFuture future = scheduledFuture; + ScheduledFuture future = connectionJob; if (future != null) { - future.cancel(true); - scheduledFuture = null; + future.cancel(false); + connectionJob = null; } } @@ -132,7 +151,7 @@ public class DeconzBridgeHandler extends BaseBridgeHandler implements WebSocketC updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING, "Allow authentication for 3rd party apps. Trying again in " + POLL_FREQUENCY_SEC + " seconds"); stopTimer(); - scheduledFuture = scheduler.schedule(this::requestApiKey, POLL_FREQUENCY_SEC, TimeUnit.SECONDS); + connectionJob = scheduler.schedule(this::requestApiKey, POLL_FREQUENCY_SEC, TimeUnit.SECONDS); } else if (r.getResponseCode() == 200) { ApiKeyMessage[] response = Objects.requireNonNull(gson.fromJson(r.getBody(), ApiKeyMessage[].class)); if (response.length == 0) { @@ -171,7 +190,7 @@ public class DeconzBridgeHandler extends BaseBridgeHandler implements WebSocketC String url = buildUrl(config.getHostWithoutPort(), config.httpPort, config.apikey); return http.get(url, config.timeout).thenApply(r -> { if (r.getResponseCode() == 403) { - return Optional. empty(); + return Optional.ofNullable((BridgeFullState) null); } else if (r.getResponseCode() == 200) { return Optional.ofNullable(gson.fromJson(r.getBody(), BridgeFullState.class)); } else { @@ -225,11 +244,11 @@ public class DeconzBridgeHandler extends BaseBridgeHandler implements WebSocketC // Use requested websocket port if no specific port is given websocketPort = config.port == 0 ? state.config.websocketport : config.port; - startWebsocket(); + startWebSocketConnection(); }, () -> { // initial response was empty, re-trying in POLL_FREQUENCY_SEC seconds if (!thingDisposing) { - scheduledFuture = scheduler.schedule(this::initializeBridgeState, POLL_FREQUENCY_SEC, TimeUnit.SECONDS); + connectionJob = scheduler.schedule(this::initializeBridgeState, POLL_FREQUENCY_SEC, TimeUnit.SECONDS); } })).exceptionally(e -> { if (e != null) { @@ -239,7 +258,7 @@ public class DeconzBridgeHandler extends BaseBridgeHandler implements WebSocketC } logger.warn("Initial full state request or result parsing failed", e); if (!thingDisposing) { - scheduledFuture = scheduler.schedule(this::initializeBridgeState, POLL_FREQUENCY_SEC, TimeUnit.SECONDS); + connectionJob = scheduler.schedule(this::initializeBridgeState, POLL_FREQUENCY_SEC, TimeUnit.SECONDS); } return null; }); @@ -249,15 +268,16 @@ public class DeconzBridgeHandler extends BaseBridgeHandler implements WebSocketC * Starts the websocket connection. * {@link #initializeBridgeState} need to be called first to obtain the websocket port. */ - private void startWebsocket() { - if (websocket.isConnected() || websocketPort == 0 || thingDisposing) { + private void startWebSocketConnection() { + ignoreConnectionLost = false; + if (webSocketConnection.isConnected() || websocketPort == 0 || thingDisposing) { return; } stopTimer(); - scheduledFuture = scheduler.schedule(this::startWebsocket, POLL_FREQUENCY_SEC, TimeUnit.SECONDS); + connectionJob = scheduler.schedule(this::startWebSocketConnection, POLL_FREQUENCY_SEC, TimeUnit.SECONDS); - websocket.start(config.getHostWithoutPort() + ":" + websocketPort); + webSocketConnection.start(config.getHostWithoutPort() + ":" + websocketPort); } /** @@ -281,6 +301,8 @@ public class DeconzBridgeHandler extends BaseBridgeHandler implements WebSocketC logger.debug("Start initializing bridge {}", thing.getUID()); thingDisposing = false; config = getConfigAs(DeconzBridgeConfig.class); + webSocketConnection.setWatchdogInterval(config.websocketTimeout); + updateStatus(ThingStatus.UNKNOWN); if (config.apikey == null) { requestApiKey(); } else { @@ -292,29 +314,37 @@ public class DeconzBridgeHandler extends BaseBridgeHandler implements WebSocketC public void dispose() { thingDisposing = true; stopTimer(); - websocket.close(); + webSocketConnection.dispose(); } @Override - public void connectionEstablished() { + public void webSocketConnectionEstablished() { stopTimer(); updateStatus(ThingStatus.ONLINE); } @Override - public void connectionLost(String reason) { + public void webSocketConnectionLost(String reason) { + if (ignoreConnectionLost) { + return; + } + ignoreConnectionLost = true; updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, reason); - stopTimer(); + + // make sure we get a new connection + webSocketConnection.dispose(); + webSocketConnection = createNewWebSocketConnection(); + // Wait for POLL_FREQUENCY_SEC after a connection was closed before trying again - scheduledFuture = scheduler.schedule(this::startWebsocket, POLL_FREQUENCY_SEC, TimeUnit.SECONDS); + connectionJob = scheduler.schedule(this::startWebSocketConnection, POLL_FREQUENCY_SEC, TimeUnit.SECONDS); } /** * Return the websocket connection. */ - public WebSocketConnection getWebsocketConnection() { - return websocket; + public WebSocketConnection getWebSocketConnection() { + return webSocketConnection; } /** @@ -322,13 +352,23 @@ public class DeconzBridgeHandler extends BaseBridgeHandler implements WebSocketC * * @param endPoint the endpoint (e.g. "lights/2/state") * @param object the object (or null if no object) + * @param httpMethod the HTTP Method * @return CompletableFuture of the result */ - public CompletableFuture sendObject(String endPoint, @Nullable Object object) { + public CompletableFuture sendObject(String endPoint, @Nullable Object object, + HttpMethod httpMethod) { String json = object == null ? null : gson.toJson(object); String url = buildUrl(config.host, config.httpPort, config.apikey, endPoint); - logger.trace("Sending {} via {}", json, url); + logger.trace("Sending {} via {} to {}", json, httpMethod, url); - return http.put(url, json, config.timeout); + if (httpMethod == HttpMethod.PUT) { + return http.put(url, json, config.timeout); + } else if (httpMethod == HttpMethod.POST) { + return http.post(url, json, config.timeout); + } else if (httpMethod == HttpMethod.DELETE) { + return http.delete(url, config.timeout); + } + + return CompletableFuture.failedFuture(new IllegalArgumentException("Unknown HTTP Method")); } } 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 index 02db57957..04a8144d5 100644 --- 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 @@ -13,14 +13,19 @@ package org.openhab.binding.deconz.internal.handler; import static org.openhab.binding.deconz.internal.BindingConstants.*; +import static org.openhab.binding.deconz.internal.Util.constrainToRange; +import static org.openhab.binding.deconz.internal.Util.kelvinToMired; +import java.util.Collection; import java.util.Map; import java.util.Set; +import java.util.concurrent.CompletableFuture; import java.util.stream.Collectors; import org.eclipse.jdt.annotation.NonNullByDefault; import org.openhab.binding.deconz.internal.DeconzDynamicCommandDescriptionProvider; import org.openhab.binding.deconz.internal.Util; +import org.openhab.binding.deconz.internal.action.GroupActions; import org.openhab.binding.deconz.internal.dto.DeconzBaseMessage; import org.openhab.binding.deconz.internal.dto.GroupAction; import org.openhab.binding.deconz.internal.dto.GroupMessage; @@ -32,12 +37,17 @@ 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.library.types.StringType; +import org.openhab.core.thing.Channel; 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.thing.binding.ThingHandlerService; import org.openhab.core.types.Command; import org.openhab.core.types.RefreshType; +import org.openhab.core.types.State; +import org.openhab.core.types.UnDefType; +import org.openhab.core.util.ColorUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -81,71 +91,83 @@ public class GroupThingHandler extends DeconzBaseThingHandler { GroupAction newGroupAction = new GroupAction(); switch (channelId) { - case CHANNEL_ALL_ON: - case CHANNEL_ANY_ON: + case CHANNEL_ALL_ON, CHANNEL_ANY_ON -> { if (command instanceof RefreshType) { - valueUpdated(channelUID.getId(), groupStateCache); + valueUpdated(channelUID, groupStateCache); return; } - break; - case CHANNEL_ALERT: + } + case CHANNEL_ALERT -> { if (command instanceof StringType) { newGroupAction.alert = command.toString(); } else { return; } - break; - case CHANNEL_COLOR: - if (command instanceof HSBType) { - HSBType hsbCommand = (HSBType) command; + } + case CHANNEL_COLOR -> { + if (command instanceof OnOffType) { + newGroupAction.on = (command == OnOffType.ON); + } else if (command instanceof HSBType hsbCommand) { // XY color is the implicit default: Use XY color mode if i) no color mode is set or ii) if the bulb // is in CT mode or iii) already in XY mode. Only if the bulb is in HS mode, use this one. if ("hs".equals(colorMode)) { newGroupAction.hue = (int) (hsbCommand.getHue().doubleValue() * HUE_FACTOR); newGroupAction.sat = Util.fromPercentType(hsbCommand.getSaturation()); + newGroupAction.bri = Util.fromPercentType(hsbCommand.getBrightness()); } else { - PercentType[] xy = hsbCommand.toXY(); - if (xy.length < 2) { - logger.warn("Failed to convert {} to xy-values", command); - } - newGroupAction.xy = new double[] { xy[0].doubleValue() / 100.0, xy[1].doubleValue() / 100.0 }; + double[] xy = ColorUtil.hsbToXY(hsbCommand); + newGroupAction.xy = new double[] { xy[0], xy[1] }; + newGroupAction.bri = (int) (xy[2] * BRIGHTNESS_MAX); } } 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; + + // send on/off state together with brightness if not already set or unknown + Integer newBri = newGroupAction.bri; + if (newBri != null) { + newGroupAction.on = (newBri > 0); } - break; - case CHANNEL_SCENE: + Double transitiontime = config.transitiontime; + if (transitiontime != null) { + // value is in 1/10 seconds + newGroupAction.transitiontime = (int) Math.round(10 * transitiontime); + } + } + case CHANNEL_COLOR_TEMPERATURE -> { + if (command instanceof DecimalType decimalCommand) { + int miredValue = kelvinToMired(decimalCommand.intValue()); + newGroupAction.ct = constrainToRange(miredValue, ZCL_CT_MIN, ZCL_CT_MAX); + newGroupAction.on = true; + } + } + case CHANNEL_SCENE -> { if (command instanceof StringType) { - String sceneId = scenes.get(command.toString()); - if (sceneId != null) { - sendCommand(null, command, channelUID, "scenes/" + sceneId + "/recall", null); - } else { - logger.debug("Ignoring command {} for {}, scene is not found in available scenes: {}", command, - channelUID, scenes); - } + getIdFromSceneName(command.toString()) + .thenAccept(id -> sendCommand(null, command, channelUID, "scenes/" + id + "/recall", null)) + .exceptionally(e -> { + logger.debug("Ignoring command {} for {}, scene is not found in available scenes {}.", + command, channelUID, scenes); + return null; + }); } return; - default: + } + default -> { + // no supported command return; + } } - Integer bri = newGroupAction.bri; - if (bri != null) { - newGroupAction.on = (bri > 0); + Boolean newOn = newGroupAction.on; + if (newOn != null && !newOn) { + // if light shall be off, no other commands are allowed, so reset the new light state + newGroupAction.clear(); + newGroupAction.on = false; } sendCommand(newGroupAction, command, channelUID, null); @@ -153,38 +175,25 @@ public class GroupThingHandler extends DeconzBaseThingHandler { @Override protected void processStateResponse(DeconzBaseMessage stateResponse) { - if (stateResponse instanceof GroupMessage) { - GroupMessage groupMessage = (GroupMessage) stateResponse; - scenes = groupMessage.scenes.stream().collect(Collectors.toMap(scene -> scene.name, scene -> scene.id)); - ChannelUID channelUID = new ChannelUID(thing.getUID(), CHANNEL_SCENE); - commandDescriptionProvider.setCommandOptions(channelUID, - groupMessage.scenes.stream().map(Scene::toCommandOption).collect(Collectors.toList())); - - } - messageReceived(config.id, stateResponse); + scenes = processScenes(stateResponse); + messageReceived(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: + private void valueUpdated(ChannelUID channelUID, GroupState newState) { + switch (channelUID.getId()) { + case CHANNEL_ALL_ON -> updateSwitchChannel(channelUID, newState.allOn); + case CHANNEL_ANY_ON -> updateSwitchChannel(channelUID, newState.anyOn); } } @Override - public void messageReceived(String sensorID, DeconzBaseMessage message) { - if (message instanceof GroupMessage) { - GroupMessage groupMessage = (GroupMessage) message; + public void messageReceived(DeconzBaseMessage message) { + if (message instanceof GroupMessage groupMessage) { 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)); + thing.getChannels().stream().map(Channel::getUID).forEach(c -> valueUpdated(c, groupState)); groupStateCache = groupState; } GroupAction groupAction = groupMessage.action; @@ -197,6 +206,68 @@ public class GroupThingHandler extends DeconzBaseThingHandler { } } } + } else { + logger.trace("{} received {}", thing.getUID(), message); + getSceneNameFromId(message.scid).thenAccept(v -> updateState(CHANNEL_SCENE, v)); } } + + private CompletableFuture getIdFromSceneName(String sceneName) { + CompletableFuture f = new CompletableFuture<>(); + + Util.getKeysFromValue(scenes, sceneName).findAny().ifPresentOrElse(f::complete, () -> { + // we need to check if that is a new scene + logger.trace("Scene name {} not found in {}, refreshing scene list", sceneName, thing.getUID()); + requestState(stateResponse -> { + scenes = processScenes(stateResponse); + Util.getKeysFromValue(scenes, sceneName).findAny().ifPresentOrElse(f::complete, + () -> f.completeExceptionally(new IllegalArgumentException("Scene not found"))); + }); + }); + + return f; + } + + private CompletableFuture getSceneNameFromId(String sceneId) { + CompletableFuture f = new CompletableFuture<>(); + + String sceneName = scenes.get(sceneId); + if (sceneName != null) { + // we already know that name, exit early + f.complete(new StringType(sceneName)); + } else { + // we need to check if that is a new scene + logger.trace("Scene name for id {} not found in {}, refreshing scene list", sceneId, thing.getUID()); + requestState(stateResponse -> { + scenes = processScenes(stateResponse); + String newSceneId = scenes.get(sceneId); + if (newSceneId != null) { + f.complete(new StringType(newSceneId)); + } else { + logger.debug("Scene name for id {} not found in {} even after refreshing scene list.", sceneId, + thing.getUID()); + f.complete(UnDefType.UNDEF); + } + }); + } + + return f; + } + + private Map processScenes(DeconzBaseMessage stateResponse) { + if (stateResponse instanceof GroupMessage groupMessage) { + Map scenes = groupMessage.scenes.stream() + .collect(Collectors.toMap(scene -> scene.id, scene -> scene.name)); + ChannelUID channelUID = new ChannelUID(thing.getUID(), CHANNEL_SCENE); + commandDescriptionProvider.setCommandOptions(channelUID, + groupMessage.scenes.stream().map(Scene::toCommandOption).collect(Collectors.toList())); + return scenes; + } + return Map.of(); + } + + @Override + public Collection> getServices() { + return Set.of(GroupActions.class); + } } 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 70a9bf574..c6e903dc4 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 @@ -41,18 +41,20 @@ import org.openhab.core.library.types.StopMoveType; import org.openhab.core.library.types.StringType; import org.openhab.core.library.types.UpDownType; import org.openhab.core.library.unit.Units; +import org.openhab.core.thing.Channel; import org.openhab.core.thing.ChannelUID; import org.openhab.core.thing.Thing; import org.openhab.core.thing.ThingStatus; import org.openhab.core.thing.ThingStatusDetail; import org.openhab.core.thing.ThingTypeUID; +import org.openhab.core.thing.binding.builder.ThingBuilder; import org.openhab.core.thing.type.ChannelKind; import org.openhab.core.types.Command; import org.openhab.core.types.CommandOption; import org.openhab.core.types.RefreshType; import org.openhab.core.types.StateDescriptionFragment; import org.openhab.core.types.StateDescriptionFragmentBuilder; -import org.openhab.core.types.UnDefType; +import org.openhab.core.util.ColorUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -94,8 +96,7 @@ public class LightThingHandler extends DeconzBaseThingHandler { */ private LightState lightStateCache = new LightState(); private LightState lastCommand = new LightState(); - @Nullable - private Integer onTime = null; // in 0.1s + private @Nullable Integer onTime = null; // in 0.1s private String colorMode = ""; // set defaults, we can override them later if we receive better values @@ -139,8 +140,8 @@ public class LightThingHandler extends DeconzBaseThingHandler { @Override public void handleCommand(ChannelUID channelUID, Command command) { if (channelUID.getId().equals(CHANNEL_ONTIME)) { - if (command instanceof QuantityType) { - QuantityType onTimeSeconds = ((QuantityType) command).toUnit(Units.SECOND); + if (command instanceof QuantityType quantity) { + QuantityType onTimeSeconds = quantity.toUnit(Units.SECOND); if (onTimeSeconds != null) { onTime = 10 * onTimeSeconds.intValue(); } else { @@ -152,7 +153,7 @@ public class LightThingHandler extends DeconzBaseThingHandler { } if (command instanceof RefreshType) { - valueUpdated(channelUID.getId(), lightStateCache); + valueUpdated(channelUID, lightStateCache); return; } @@ -161,14 +162,14 @@ public class LightThingHandler extends DeconzBaseThingHandler { Integer currentBri = lightStateCache.bri; switch (channelUID.getId()) { - case CHANNEL_ALERT: + case CHANNEL_ALERT -> { if (command instanceof StringType) { newLightState.alert = command.toString(); } else { return; } - break; - case CHANNEL_EFFECT: + } + case CHANNEL_EFFECT -> { if (command instanceof StringType) { // effect command only allowed for lights that are turned on newLightState.on = true; @@ -176,25 +177,23 @@ public class LightThingHandler extends DeconzBaseThingHandler { } else { return; } - break; - case CHANNEL_EFFECT_SPEED: + } + case CHANNEL_EFFECT_SPEED -> { if (command instanceof DecimalType) { newLightState.on = true; newLightState.effectSpeed = Util.constrainToRange(((DecimalType) command).intValue(), 0, 10); } else { return; } - break; - case CHANNEL_SWITCH: - case CHANNEL_LOCK: + } + case CHANNEL_SWITCH, CHANNEL_LOCK -> { if (command instanceof OnOffType) { newLightState.on = (command == OnOffType.ON); } else { return; } - break; - case CHANNEL_BRIGHTNESS: - case CHANNEL_COLOR: + } + case CHANNEL_BRIGHTNESS, CHANNEL_COLOR -> { if (command instanceof OnOffType) { newLightState.on = (command == OnOffType.ON); } else if (command instanceof IncreaseDecreaseType) { @@ -208,21 +207,18 @@ public class LightThingHandler extends DeconzBaseThingHandler { newLightState.bri = Util.constrainToRange(oldBri - BRIGHTNESS_DIM_STEP, BRIGHTNESS_MIN, BRIGHTNESS_MAX); } - } else if (command instanceof HSBType) { - HSBType hsbCommand = (HSBType) command; + } else if (command instanceof HSBType hsbCommand) { // XY color is the implicit default: Use XY color mode if i) no color mode is set or ii) if the bulb // is in CT mode or iii) already in XY mode. Only if the bulb is in HS mode, use this one. if ("hs".equals(colorMode)) { newLightState.hue = (int) (hsbCommand.getHue().doubleValue() * HUE_FACTOR); newLightState.sat = Util.fromPercentType(hsbCommand.getSaturation()); + newLightState.bri = Util.fromPercentType(hsbCommand.getBrightness()); } else { - PercentType[] xy = hsbCommand.toXY(); - if (xy.length < 2) { - logger.warn("Failed to convert {} to xy-values", command); - } - newLightState.xy = new double[] { xy[0].doubleValue() / 100.0, xy[1].doubleValue() / 100.0 }; + double[] xy = ColorUtil.hsbToXY(hsbCommand); + newLightState.xy = new double[] { xy[0], xy[1] }; + newLightState.bri = (int) (xy[2] * BRIGHTNESS_MAX); } - newLightState.bri = Util.fromPercentType(hsbCommand.getBrightness()); } else if (command instanceof PercentType) { newLightState.bri = Util.fromPercentType((PercentType) command); } else if (command instanceof DecimalType) { @@ -241,40 +237,34 @@ public class LightThingHandler extends DeconzBaseThingHandler { if (newBri != null && newBri == 0 && currentOn != null && !currentOn) { return; } - Double transitiontime = config.transitiontime; if (transitiontime != null) { // value is in 1/10 seconds newLightState.transitiontime = (int) Math.round(10 * transitiontime); } - break; - case CHANNEL_COLOR_TEMPERATURE: + } + case CHANNEL_COLOR_TEMPERATURE -> { if (command instanceof DecimalType) { int miredValue = kelvinToMired(((DecimalType) command).intValue()); newLightState.ct = constrainToRange(miredValue, ctMin, ctMax); newLightState.on = true; } - break; - case CHANNEL_POSITION: + } + case CHANNEL_POSITION -> { if (command instanceof UpDownType) { - newLightState.on = (command == UpDownType.DOWN); + newLightState.open = (command == UpDownType.UP); } else if (command == StopMoveType.STOP) { - if (currentOn != null && currentOn && currentBri != null && currentBri <= BRIGHTNESS_MAX) { - // going down or currently stop (254 because of rounding error) - newLightState.on = true; - } else if (currentOn != null && !currentOn && currentBri != null && currentBri > BRIGHTNESS_MIN) { - // going up or currently stopped - newLightState.on = false; - } + newLightState.stop = true; } else if (command instanceof PercentType) { - newLightState.bri = fromPercentType((PercentType) command); + newLightState.lift = ((PercentType) command).intValue(); } else { return; } - break; - default: + } + default -> { // no supported command return; + } } Boolean newOn = newLightState.on; @@ -296,12 +286,10 @@ public class LightThingHandler extends DeconzBaseThingHandler { @Override protected void processStateResponse(DeconzBaseMessage stateResponse) { - if (!(stateResponse instanceof LightMessage)) { + if (!(stateResponse instanceof LightMessage lightMessage)) { return; } - LightMessage lightMessage = (LightMessage) stateResponse; - if (needsPropertyUpdate) { // if we did not receive a ctmin/ctmax, then we probably don't need it needsPropertyUpdate = false; @@ -316,41 +304,60 @@ public class LightThingHandler extends DeconzBaseThingHandler { } } + ThingBuilder thingBuilder = editThing(); + boolean thingEdited = false; + LightState lightState = lightMessage.state; - if (lightState != null && lightState.effect != null) { - checkAndUpdateEffectChannels(lightMessage); + if (lightState != null && lightState.effect != null + && checkAndUpdateEffectChannels(thingBuilder, lightMessage)) { + thingEdited = true; } - messageReceived(config.id, lightMessage); + if (checkLastSeen(thingBuilder, stateResponse.lastseen)) { + thingEdited = true; + } + if (thingEdited) { + updateThing(thingBuilder.build()); + } + + messageReceived(lightMessage); } private enum EffectLightModel { LIDL_MELINARA, TINT_MUELLER, - UNKNOWN; + UNKNOWN } - private void checkAndUpdateEffectChannels(LightMessage lightMessage) { - EffectLightModel model = EffectLightModel.UNKNOWN; + private boolean checkAndUpdateEffectChannels(ThingBuilder thingBuilder, LightMessage lightMessage) { // try to determine which model we have - if (lightMessage.manufacturername.equals("_TZE200_s8gkrkxk")) { - // the LIDL Melinara string does not report a proper model name - model = EffectLightModel.LIDL_MELINARA; - } else if (lightMessage.manufacturername.equals("MLI")) { - model = EffectLightModel.TINT_MUELLER; - } else { + EffectLightModel model = switch (lightMessage.manufacturername) { + case "_TZE200_s8gkrkxk" -> EffectLightModel.LIDL_MELINARA; + case "MLI" -> EffectLightModel.TINT_MUELLER; + default -> EffectLightModel.UNKNOWN; + }; + if (model == EffectLightModel.UNKNOWN) { logger.debug( "Could not determine effect light type for thing {}, if you feel this is wrong request adding support on GitHub.", thing.getUID()); } ChannelUID effectChannelUID = new ChannelUID(thing.getUID(), CHANNEL_EFFECT); - createChannel(CHANNEL_EFFECT, ChannelKind.STATE); + + boolean thingEdited = false; + + if (thing.getChannel(CHANNEL_EFFECT) == null) { + createChannel(thingBuilder, CHANNEL_EFFECT, ChannelKind.STATE); + thingEdited = true; + } switch (model) { case LIDL_MELINARA: - // additional channels - createChannel(CHANNEL_EFFECT_SPEED, ChannelKind.STATE); + if (thing.getChannel(CHANNEL_EFFECT_SPEED) == null) { + // additional channels + createChannel(thingBuilder, CHANNEL_EFFECT_SPEED, ChannelKind.STATE); + thingEdited = true; + } List options = List.of("none", "steady", "snow", "rainbow", "snake", "tinkle", "fireworks", "flag", "waves", "updown", "vintage", "fading", "collide", "strobe", "sparkles", "carnival", @@ -366,84 +373,38 @@ public class LightThingHandler extends DeconzBaseThingHandler { options = List.of("none", "colorloop"); commandDescriptionProvider.setCommandOptions(effectChannelUID, toCommandOptionList(options)); } + + return thingEdited; } private List toCommandOptionList(List options) { return options.stream().map(c -> new CommandOption(c, c)).collect(Collectors.toList()); } - private void valueUpdated(String channelId, LightState newState) { - Integer bri = newState.bri; - Integer hue = newState.hue; - Integer sat = newState.sat; + private void valueUpdated(ChannelUID channelUID, LightState newState) { Boolean on = newState.on; - switch (channelId) { - case CHANNEL_ALERT: - String alert = newState.alert; - if (alert != null) { - updateState(channelId, new StringType(alert)); - } - break; - case CHANNEL_SWITCH: - case CHANNEL_LOCK: - if (on != null) { - updateState(channelId, OnOffType.from(on)); - } - break; - case CHANNEL_COLOR: - if (on != null && !on) { - updateState(channelId, OnOffType.OFF); - } else if (bri != null && "xy".equals(newState.colormode)) { - final double @Nullable [] xy = newState.xy; - if (xy != null && xy.length == 2) { - HSBType color = HSBType.fromXY((float) xy[0], (float) xy[1]); - updateState(channelId, new HSBType(color.getHue(), color.getSaturation(), toPercentType(bri))); - } - } else if (bri != null && hue != null && sat != null) { - updateState(channelId, - new HSBType(new DecimalType(hue / HUE_FACTOR), toPercentType(sat), toPercentType(bri))); - } - break; - case CHANNEL_BRIGHTNESS: - if (bri != null && on != null && on) { - updateState(channelId, toPercentType(bri)); - } else { - updateState(channelId, OnOffType.OFF); - } - break; - case CHANNEL_COLOR_TEMPERATURE: + switch (channelUID.getId()) { + case CHANNEL_ALERT -> updateStringChannel(channelUID, newState.alert); + case CHANNEL_SWITCH, CHANNEL_LOCK -> updateSwitchChannel(channelUID, on); + case CHANNEL_COLOR -> updateColorChannel(channelUID, newState); + case CHANNEL_BRIGHTNESS -> updatePercentTypeChannel(channelUID, newState.bri, newState.on); + case CHANNEL_COLOR_TEMPERATURE -> { Integer ct = newState.ct; if (ct != null && ct >= ctMin && ct <= ctMax) { - updateState(channelId, new DecimalType(miredToKelvin(ct))); + updateState(channelUID, new DecimalType(miredToKelvin(ct))); } - break; - case CHANNEL_POSITION: - if (bri != null) { - updateState(channelId, toPercentType(bri)); - } - break; - case CHANNEL_EFFECT: - String effect = newState.effect; - if (effect != null) { - updateState(channelId, new StringType(effect)); - } - break; - case CHANNEL_EFFECT_SPEED: - Integer effectSpeed = newState.effectSpeed; - if (effectSpeed != null) { - updateState(channelId, new DecimalType(effectSpeed)); - } - break; - default: + } + case CHANNEL_POSITION -> updatePercentTypeChannel(channelUID, newState.bri, true); // always post value + case CHANNEL_EFFECT -> updateStringChannel(channelUID, newState.effect); + case CHANNEL_EFFECT_SPEED -> updateDecimalTypeChannel(channelUID, newState.effectSpeed); } } @Override - public void messageReceived(String sensorID, DeconzBaseMessage message) { - if (message instanceof LightMessage) { - LightMessage lightMessage = (LightMessage) message; - logger.trace("{} received {}", thing.getUID(), lightMessage); + public void messageReceived(DeconzBaseMessage message) { + logger.trace("{} received {}", thing.getUID(), message); + if (message instanceof LightMessage lightMessage) { LightState lightState = lightMessage.state; if (lightState != null) { if (lastCommandExpireTimestamp > System.currentTimeMillis() @@ -462,12 +423,34 @@ public class LightThingHandler extends DeconzBaseThingHandler { lightStateCache = lightState; if (Boolean.TRUE.equals(lightState.reachable)) { updateStatus(ThingStatus.ONLINE); - thing.getChannels().stream().map(c -> c.getUID().getId()).forEach(c -> valueUpdated(c, lightState)); + thing.getChannels().stream().map(Channel::getUID).forEach(c -> valueUpdated(c, lightState)); } else { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "@text/offline.light-not-reachable"); - thing.getChannels().stream().map(c -> c.getUID()).forEach(c -> updateState(c, UnDefType.UNDEF)); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "@text/offline.sensor-not-reachable"); } } } } + + private void updateColorChannel(ChannelUID channelUID, LightState newState) { + Boolean on = newState.on; + Integer bri = newState.bri; + Integer hue = newState.hue; + Integer sat = newState.sat; + + if (on != null && !on) { + updateState(channelUID, OnOffType.OFF); + } else if (bri != null && "xy".equals(newState.colormode)) { + final double @Nullable [] xy = newState.xy; + if (xy != null && xy.length == 2) { + double[] xyY = new double[3]; + xyY[0] = xy[0]; + xyY[1] = xy[1]; + xyY[2] = ((double) bri) / BRIGHTNESS_MAX; + updateState(channelUID, ColorUtil.xyToHsv(xyY)); + } + } else if (bri != null && hue != null && sat != null) { + updateState(channelUID, + new HSBType(new DecimalType(hue / HUE_FACTOR), toPercentType(sat), toPercentType(bri))); + } + } } 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 55aceb248..004d769d0 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 @@ -17,28 +17,20 @@ import static org.openhab.binding.deconz.internal.BindingConstants.*; import java.util.List; import java.util.Map; import java.util.Objects; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.TimeUnit; - -import javax.measure.Unit; 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.SensorConfig; import org.openhab.binding.deconz.internal.dto.SensorMessage; import org.openhab.binding.deconz.internal.dto.SensorState; 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; -import org.openhab.core.library.types.StringType; import org.openhab.core.thing.Channel; import org.openhab.core.thing.ChannelUID; import org.openhab.core.thing.Thing; import org.openhab.core.thing.ThingStatus; import org.openhab.core.thing.ThingStatusDetail; +import org.openhab.core.thing.binding.builder.ThingBuilder; import org.openhab.core.thing.type.ChannelKind; import org.openhab.core.types.Command; import org.slf4j.Logger; @@ -73,27 +65,16 @@ public abstract class SensorBaseThingHandler extends DeconzBaseThingHandler { * Prevent a dispose/init cycle while this flag is set. Use for property updates */ private boolean ignoreConfigurationUpdate; - private @Nullable ScheduledFuture lastSeenPollingJob; public SensorBaseThingHandler(Thing thing, Gson gson) { super(thing, gson, ResourceType.SENSORS); } - @Override - public void dispose() { - ScheduledFuture lastSeenPollingJob = this.lastSeenPollingJob; - if (lastSeenPollingJob != null) { - lastSeenPollingJob.cancel(true); - this.lastSeenPollingJob = null; - } - - super.dispose(); - } - @Override public abstract void handleCommand(ChannelUID channelUID, Command command); - protected abstract void createTypeSpecificChannels(SensorConfig sensorState, SensorState sensorConfig); + protected abstract boolean createTypeSpecificChannels(ThingBuilder thingBuilder, SensorConfig sensorState, + SensorState sensorConfig); protected abstract List getConfigChannels(); @@ -106,11 +87,10 @@ public abstract class SensorBaseThingHandler extends DeconzBaseThingHandler { @Override protected void processStateResponse(DeconzBaseMessage stateResponse) { - if (!(stateResponse instanceof SensorMessage)) { + if (!(stateResponse instanceof SensorMessage sensorMessage)) { return; } - SensorMessage sensorMessage = (SensorMessage) stateResponse; sensorConfig = Objects.requireNonNullElse(sensorMessage.config, new SensorConfig()); sensorState = Objects.requireNonNullElse(sensorMessage.state, new SensorState()); @@ -133,34 +113,38 @@ public abstract class SensorBaseThingHandler extends DeconzBaseThingHandler { // Some sensors support optional channels // (see https://github.com/dresden-elektronik/deconz-rest-plugin/wiki/Supported-Devices#sensors) // any battery-powered sensor + ThingBuilder thingBuilder = editThing(); + boolean thingEdited = false; + if (sensorConfig.battery != null) { - createChannel(CHANNEL_BATTERY_LEVEL, ChannelKind.STATE); - createChannel(CHANNEL_BATTERY_LOW, ChannelKind.STATE); + if (createChannel(thingBuilder, CHANNEL_BATTERY_LEVEL, ChannelKind.STATE)) { + thingEdited = true; + } + if (createChannel(thingBuilder, CHANNEL_BATTERY_LOW, ChannelKind.STATE)) { + thingEdited = true; + } + } else if (sensorState.lowbattery != null) { + // if sensorConfig.battery != null the channel is already added + if (createChannel(thingBuilder, CHANNEL_BATTERY_LOW, ChannelKind.STATE)) { + thingEdited = true; + } } - if (sensorState.lowbattery != null) { - createChannel(CHANNEL_BATTERY_LOW, ChannelKind.STATE); + if (createTypeSpecificChannels(thingBuilder, sensorConfig, sensorState)) { + thingEdited = true; } - createTypeSpecificChannels(sensorConfig, sensorState); + if (checkLastSeen(thingBuilder, sensorMessage.lastseen)) { + thingEdited = true; + } + // if the thing was edited, we update it now + if (thingEdited) { + logger.debug("Thing configuration changed, updating thing."); + updateThing(thingBuilder.build()); + } ignoreConfigurationUpdate = false; - // "Last seen" is the last "ping" from the device, whereas "last update" is the last status changed. - // For example, for a fire sensor, the device pings regularly, without necessarily updating channels. - // So to monitor a sensor is still alive, the "last seen" is necessary. - // Because "last seen" is never updated by the WebSocket API - if this is supported, then we have to - // manually poll it after the defined time - String lastSeen = sensorMessage.lastseen; - if (lastSeen != null && config.lastSeenPolling > 0) { - createChannel(CHANNEL_LAST_SEEN, ChannelKind.STATE); - updateState(CHANNEL_LAST_SEEN, Util.convertTimestampToDateTime(lastSeen)); - lastSeenPollingJob = scheduler.schedule(() -> requestState(this::processLastSeen), config.lastSeenPolling, - TimeUnit.MINUTES); - logger.trace("lastSeen polling enabled for thing {} with interval of {} minutes", thing.getUID(), - config.lastSeenPolling); - } - // Initial data updateChannels(sensorConfig); updateChannels(sensorState, true); @@ -168,13 +152,6 @@ public abstract class SensorBaseThingHandler extends DeconzBaseThingHandler { updateStatus(ThingStatus.ONLINE); } - private void processLastSeen(DeconzBaseMessage stateResponse) { - String lastSeen = stateResponse.lastseen; - if (lastSeen != null) { - updateState(CHANNEL_LAST_SEEN, Util.convertTimestampToDateTime(lastSeen)); - } - } - /** * Update channel value from {@link SensorConfig} object - override to include further channels * @@ -183,19 +160,12 @@ public abstract class SensorBaseThingHandler extends DeconzBaseThingHandler { */ protected void valueUpdated(ChannelUID channelUID, SensorConfig newConfig) { Integer batteryLevel = newConfig.battery; - switch (channelUID.getId()) { - case CHANNEL_BATTERY_LEVEL: - if (batteryLevel != null) { - updateState(channelUID, new DecimalType(batteryLevel.longValue())); - } - break; - case CHANNEL_BATTERY_LOW: - if (batteryLevel != null) { - updateState(channelUID, OnOffType.from(batteryLevel <= 10)); - } - break; - default: - // other cases covered by sub-class + if (batteryLevel != null) { + switch (channelUID.getId()) { + case CHANNEL_BATTERY_LEVEL -> updateDecimalTypeChannel(channelUID, batteryLevel.longValue()); + case CHANNEL_BATTERY_LOW -> updateSwitchChannel(channelUID, batteryLevel <= 10); + // other cases covered by subclass + } } } @@ -208,32 +178,29 @@ public abstract class SensorBaseThingHandler extends DeconzBaseThingHandler { */ protected void valueUpdated(ChannelUID channelUID, SensorState newState, boolean initializing) { switch (channelUID.getId()) { - case CHANNEL_LAST_UPDATED: + case CHANNEL_LAST_UPDATED -> { String lastUpdated = newState.lastupdated; if (lastUpdated != null && !"none".equals(lastUpdated)) { updateState(channelUID, Util.convertTimestampToDateTime(lastUpdated)); } - break; - case CHANNEL_BATTERY_LOW: - Boolean lowBattery = newState.lowbattery; - if (lowBattery != null) { - updateState(channelUID, OnOffType.from(lowBattery)); - } - break; - default: - // other cases covered by sub-class + } + case CHANNEL_BATTERY_LOW -> updateSwitchChannel(channelUID, newState.lowbattery); + // other cases covered by subclass } } @Override - public void messageReceived(String sensorID, DeconzBaseMessage message) { + public void messageReceived(DeconzBaseMessage message) { logger.trace("{} received {}", thing.getUID(), message); - if (message instanceof SensorMessage) { - SensorMessage sensorMessage = (SensorMessage) message; + if (message instanceof SensorMessage sensorMessage) { SensorConfig sensorConfig = sensorMessage.config; if (sensorConfig != null) { - this.sensorConfig = sensorConfig; - updateChannels(sensorConfig); + if (sensorConfig.reachable) { + updateStatus(ThingStatus.ONLINE); + updateChannels(sensorConfig); + } else { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "@text/offline.sensor-not-reachable"); + } } SensorState sensorState = sensorMessage.state; if (sensorState != null) { @@ -243,6 +210,7 @@ public abstract class SensorBaseThingHandler extends DeconzBaseThingHandler { } private void updateChannels(SensorConfig newConfig) { + this.sensorConfig = newConfig; List configChannels = getConfigChannels(); thing.getChannels().stream().map(Channel::getUID) .filter(channelUID -> configChannels.contains(channelUID.getId())) @@ -253,34 +221,4 @@ public abstract class SensorBaseThingHandler extends DeconzBaseThingHandler { sensorState = newState; thing.getChannels().forEach(channel -> valueUpdated(channel.getUID(), newState, initializing)); } - - protected void updateSwitchChannel(ChannelUID channelUID, @Nullable Boolean value) { - if (value == null) { - return; - } - updateState(channelUID, OnOffType.from(value)); - } - - protected void updateStringChannel(ChannelUID channelUID, @Nullable String value) { - updateState(channelUID, new StringType(value)); - } - - protected void updateDecimalTypeChannel(ChannelUID channelUID, @Nullable Number value) { - if (value == null) { - return; - } - updateState(channelUID, new DecimalType(value.longValue())); - } - - protected void updateQuantityTypeChannel(ChannelUID channelUID, @Nullable Number value, Unit unit) { - updateQuantityTypeChannel(channelUID, value, unit, 1.0); - } - - protected void updateQuantityTypeChannel(ChannelUID channelUID, @Nullable Number value, Unit unit, - double scaling) { - if (value == null) { - return; - } - updateState(channelUID, new QuantityType<>(value.doubleValue() * scaling, unit)); - } } 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 9fe4cbd30..0f04d09e0 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 @@ -33,15 +33,18 @@ import org.openhab.binding.deconz.internal.dto.SensorState; import org.openhab.binding.deconz.internal.dto.ThermostatUpdateConfig; import org.openhab.binding.deconz.internal.types.ThermostatMode; import org.openhab.core.library.types.DecimalType; +import org.openhab.core.library.types.OnOffType; import org.openhab.core.library.types.OpenClosedType; import org.openhab.core.library.types.QuantityType; import org.openhab.core.library.types.StringType; import org.openhab.core.thing.ChannelUID; import org.openhab.core.thing.Thing; import org.openhab.core.thing.ThingTypeUID; +import org.openhab.core.thing.binding.builder.ThingBuilder; import org.openhab.core.thing.type.ChannelKind; import org.openhab.core.types.Command; import org.openhab.core.types.RefreshType; +import org.openhab.core.types.UnDefType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -66,7 +69,7 @@ public class SensorThermostatThingHandler extends SensorBaseThingHandler { public static final Set SUPPORTED_THING_TYPES = Collections.singleton(THING_TYPE_THERMOSTAT); private static final List CONFIG_CHANNELS = Arrays.asList(CHANNEL_BATTERY_LEVEL, CHANNEL_BATTERY_LOW, - CHANNEL_HEATSETPOINT, CHANNEL_TEMPERATURE_OFFSET, CHANNEL_THERMOSTAT_MODE); + CHANNEL_HEATSETPOINT, CHANNEL_TEMPERATURE_OFFSET, CHANNEL_THERMOSTAT_MODE, CHANNEL_THERMOSTAT_LOCKED); private final Logger logger = LoggerFactory.getLogger(SensorThermostatThingHandler.class); @@ -83,23 +86,24 @@ public class SensorThermostatThingHandler extends SensorBaseThingHandler { } ThermostatUpdateConfig newConfig = new ThermostatUpdateConfig(); switch (channelUID.getId()) { - case CHANNEL_HEATSETPOINT: + case CHANNEL_THERMOSTAT_LOCKED -> newConfig.locked = OnOffType.ON.equals(command); + case CHANNEL_HEATSETPOINT -> { Integer newHeatsetpoint = getTemperatureFromCommand(command); if (newHeatsetpoint == null) { logger.warn("Heatsetpoint must not be null."); return; } newConfig.heatsetpoint = newHeatsetpoint; - break; - case CHANNEL_TEMPERATURE_OFFSET: + } + case CHANNEL_TEMPERATURE_OFFSET -> { Integer newOffset = getTemperatureFromCommand(command); if (newOffset == null) { logger.warn("Offset must not be null."); return; } newConfig.offset = newOffset; - break; - case CHANNEL_THERMOSTAT_MODE: + } + case CHANNEL_THERMOSTAT_MODE -> { if (command instanceof StringType) { String thermostatMode = ((StringType) command).toString(); try { @@ -117,11 +121,12 @@ public class SensorThermostatThingHandler extends SensorBaseThingHandler { } else { return; } - break; - default: + } + case CHANNEL_EXTERNAL_WINDOW_OPEN -> newConfig.externalwindowopen = OpenClosedType.OPEN.equals(command); + default -> { // no supported command return; - + } } sendCommand(newConfig, command, channelUID, null); @@ -133,15 +138,18 @@ public class SensorThermostatThingHandler extends SensorBaseThingHandler { ThermostatMode thermostatMode = newConfig.mode; String mode = thermostatMode != null ? thermostatMode.name() : ThermostatMode.UNKNOWN.name(); switch (channelUID.getId()) { - case CHANNEL_HEATSETPOINT: - updateQuantityTypeChannel(channelUID, newConfig.heatsetpoint, CELSIUS, 1.0 / 100); - break; - case CHANNEL_TEMPERATURE_OFFSET: - updateQuantityTypeChannel(channelUID, newConfig.offset, CELSIUS, 1.0 / 100); - break; - case CHANNEL_THERMOSTAT_MODE: - updateState(channelUID, new StringType(mode)); - break; + case CHANNEL_THERMOSTAT_LOCKED -> updateSwitchChannel(channelUID, newConfig.locked); + case CHANNEL_HEATSETPOINT -> updateQuantityTypeChannel(channelUID, newConfig.heatsetpoint, CELSIUS, + 1.0 / 100); + case CHANNEL_TEMPERATURE_OFFSET -> updateQuantityTypeChannel(channelUID, newConfig.offset, CELSIUS, + 1.0 / 100); + case CHANNEL_THERMOSTAT_MODE -> updateState(channelUID, new StringType(mode)); + case CHANNEL_EXTERNAL_WINDOW_OPEN -> { + Boolean open = newConfig.externalwindowopen; + if (open != null) { + updateState(channelUID, open ? OpenClosedType.OPEN : OpenClosedType.CLOSED); + } + } } } @@ -149,23 +157,32 @@ public class SensorThermostatThingHandler extends SensorBaseThingHandler { protected void valueUpdated(ChannelUID channelUID, SensorState newState, boolean initializing) { super.valueUpdated(channelUID, newState, initializing); switch (channelUID.getId()) { - case CHANNEL_TEMPERATURE: - updateQuantityTypeChannel(channelUID, newState.temperature, CELSIUS, 1.0 / 100); - break; - case CHANNEL_VALVE_POSITION: - updateQuantityTypeChannel(channelUID, newState.valve, PERCENT, 100.0 / 255); - break; - case CHANNEL_WINDOWOPEN: + case CHANNEL_TEMPERATURE -> updateQuantityTypeChannel(channelUID, newState.temperature, CELSIUS, 1.0 / 100); + case CHANNEL_VALVE_POSITION -> { + Integer valve = newState.valve; + if (valve == null || valve < 0 || valve > 100) { + updateState(channelUID, UnDefType.UNDEF); + } else { + updateQuantityTypeChannel(channelUID, valve, PERCENT, 1.0); + } + } + case CHANNEL_WINDOW_OPEN -> { String open = newState.windowopen; if (open != null) { updateState(channelUID, "Closed".equals(open) ? OpenClosedType.CLOSED : OpenClosedType.OPEN); } - break; + } } } @Override - protected void createTypeSpecificChannels(SensorConfig sensorConfig, SensorState sensorState) { + protected boolean createTypeSpecificChannels(ThingBuilder thingBuilder, SensorConfig sensorConfig, + SensorState sensorState) { + boolean thingEdited = false; + if (sensorConfig.locked != null && createChannel(thingBuilder, CHANNEL_THERMOSTAT_LOCKED, ChannelKind.STATE)) { + thingEdited = true; + } + return thingEdited; } @Override @@ -193,14 +210,30 @@ public class SensorThermostatThingHandler extends SensorBaseThingHandler { @Override protected void processStateResponse(DeconzBaseMessage stateResponse) { - if (!(stateResponse instanceof SensorMessage)) { + if (!(stateResponse instanceof SensorMessage sensorMessage)) { return; } - SensorMessage sensorMessage = (SensorMessage) stateResponse; SensorState sensorState = sensorMessage.state; + SensorConfig sensorConfig = sensorMessage.config; + + boolean changed = false; + ThingBuilder thingBuilder = editThing(); + if (sensorState != null && sensorState.windowopen != null) { - createChannel(CHANNEL_WINDOWOPEN, ChannelKind.STATE); + if (createChannel(thingBuilder, CHANNEL_WINDOW_OPEN, ChannelKind.STATE)) { + changed = true; + } + } + + if (sensorConfig != null && sensorConfig.externalwindowopen != null) { + if (createChannel(thingBuilder, CHANNEL_EXTERNAL_WINDOW_OPEN, ChannelKind.STATE)) { + changed = true; + } + } + + if (changed) { + updateThing(thingBuilder.build()); } super.processStateResponse(stateResponse); 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 ca1538e1e..9dc3eb404 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 @@ -25,7 +25,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.SensorUpdateConfig; -import org.openhab.core.library.types.HSBType; import org.openhab.core.library.types.OnOffType; import org.openhab.core.library.types.OpenClosedType; import org.openhab.core.library.types.QuantityType; @@ -33,9 +32,11 @@ import org.openhab.core.library.types.StringType; import org.openhab.core.thing.ChannelUID; import org.openhab.core.thing.Thing; import org.openhab.core.thing.ThingTypeUID; +import org.openhab.core.thing.binding.builder.ThingBuilder; import org.openhab.core.thing.type.ChannelKind; import org.openhab.core.types.Command; import org.openhab.core.types.RefreshType; +import org.openhab.core.util.ColorUtil; import com.google.gson.Gson; @@ -60,7 +61,8 @@ public class SensorThingHandler extends SensorBaseThingHandler { 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_AIRQUALITY_SENSOR, THING_TYPE_COLOR_CONTROL); + THING_TYPE_CARBONMONOXIDE_SENSOR, THING_TYPE_AIRQUALITY_SENSOR, THING_TYPE_COLOR_CONTROL, + THING_TYPE_MOISTURE_SENSOR); private static final List CONFIG_CHANNELS = List.of(CHANNEL_BATTERY_LEVEL, CHANNEL_BATTERY_LOW, CHANNEL_ENABLED, CHANNEL_TEMPERATURE); @@ -91,15 +93,13 @@ public class SensorThingHandler extends SensorBaseThingHandler { protected void valueUpdated(ChannelUID channelUID, SensorConfig newConfig) { super.valueUpdated(channelUID, newConfig); switch (channelUID.getId()) { - case CHANNEL_ENABLED: - updateState(channelUID, OnOffType.from(newConfig.on)); - break; - case CHANNEL_TEMPERATURE: + case CHANNEL_ENABLED -> updateState(channelUID, OnOffType.from(newConfig.on)); + case CHANNEL_TEMPERATURE -> { Float temperature = newConfig.temperature; if (temperature != null) { updateState(channelUID, new QuantityType<>(temperature / 100, CELSIUS)); } - break; + } } } @@ -107,10 +107,8 @@ public class SensorThingHandler extends SensorBaseThingHandler { protected void valueUpdated(ChannelUID channelUID, SensorState newState, boolean initializing) { super.valueUpdated(channelUID, newState, initializing); switch (channelUID.getId()) { - case CHANNEL_BATTERY_LEVEL: - updateDecimalTypeChannel(channelUID, newState.battery); - break; - case CHANNEL_LIGHT: + case CHANNEL_BATTERY_LEVEL -> updateDecimalTypeChannel(channelUID, newState.battery); + case CHANNEL_LIGHT -> { Boolean dark = newState.dark; if (dark != null) { Boolean daylight = newState.daylight; @@ -126,138 +124,103 @@ public class SensorThingHandler extends SensorBaseThingHandler { updateState(channelUID, new StringType("Daylight")); } } - break; - case CHANNEL_POWER: - updateQuantityTypeChannel(channelUID, newState.power, WATT); - break; - case CHANNEL_CONSUMPTION: - updateQuantityTypeChannel(channelUID, newState.consumption, WATT_HOUR); - break; - case CHANNEL_VOLTAGE: - updateQuantityTypeChannel(channelUID, newState.voltage, VOLT); - break; - case CHANNEL_CURRENT: - updateQuantityTypeChannel(channelUID, newState.current, MILLI(AMPERE)); - break; - case CHANNEL_LIGHT_LUX: - updateQuantityTypeChannel(channelUID, newState.lux, LUX); - break; - case CHANNEL_COLOR: + } + case CHANNEL_POWER -> updateQuantityTypeChannel(channelUID, newState.power, WATT); + case CHANNEL_CONSUMPTION -> updateQuantityTypeChannel(channelUID, newState.consumption, WATT_HOUR); + case CHANNEL_VOLTAGE -> updateQuantityTypeChannel(channelUID, newState.voltage, VOLT); + case CHANNEL_CURRENT -> updateQuantityTypeChannel(channelUID, newState.current, MILLI(AMPERE)); + case CHANNEL_LIGHT_LUX -> updateQuantityTypeChannel(channelUID, newState.lux, LUX); + case CHANNEL_COLOR -> { final double @Nullable [] xy = newState.xy; if (xy != null && xy.length == 2) { - updateState(channelUID, HSBType.fromXY((float) xy[0], (float) xy[1])); + updateState(channelUID, ColorUtil.xyToHsv(xy)); } - break; - case CHANNEL_LIGHT_LEVEL: - updateDecimalTypeChannel(channelUID, newState.lightlevel); - break; - case CHANNEL_DARK: - updateSwitchChannel(channelUID, newState.dark); - break; - case CHANNEL_DAYLIGHT: - updateSwitchChannel(channelUID, newState.daylight); - break; - case CHANNEL_TEMPERATURE: - updateQuantityTypeChannel(channelUID, newState.temperature, CELSIUS, 1.0 / 100); - break; - case CHANNEL_HUMIDITY: - updateQuantityTypeChannel(channelUID, newState.humidity, PERCENT, 1.0 / 100); - break; - case CHANNEL_PRESSURE: - updateQuantityTypeChannel(channelUID, newState.pressure, HECTO(PASCAL)); - break; - case CHANNEL_PRESENCE: - updateSwitchChannel(channelUID, newState.presence); - break; - case CHANNEL_VALUE: - updateDecimalTypeChannel(channelUID, newState.status); - break; - case CHANNEL_OPENCLOSE: + } + case CHANNEL_LIGHT_LEVEL -> updateDecimalTypeChannel(channelUID, newState.lightlevel); + case CHANNEL_DARK -> updateSwitchChannel(channelUID, newState.dark); + case CHANNEL_DAYLIGHT -> updateSwitchChannel(channelUID, newState.daylight); + case CHANNEL_TEMPERATURE -> updateQuantityTypeChannel(channelUID, newState.temperature, CELSIUS, 1.0 / 100); + case CHANNEL_HUMIDITY -> updateQuantityTypeChannel(channelUID, newState.humidity, PERCENT, 1.0 / 100); + case CHANNEL_PRESSURE -> updateQuantityTypeChannel(channelUID, newState.pressure, HECTO(PASCAL)); + case CHANNEL_PRESENCE -> updateSwitchChannel(channelUID, newState.presence); + case CHANNEL_VALUE -> updateDecimalTypeChannel(channelUID, newState.status); + case CHANNEL_OPENCLOSE -> { Boolean open = newState.open; if (open != null) { updateState(channelUID, open ? OpenClosedType.OPEN : OpenClosedType.CLOSED); } - break; - case CHANNEL_WATERLEAKAGE: - updateSwitchChannel(channelUID, newState.water); - break; - case CHANNEL_FIRE: - updateSwitchChannel(channelUID, newState.fire); - break; - case CHANNEL_ALARM: - updateSwitchChannel(channelUID, newState.alarm); - break; - case CHANNEL_TAMPERED: - updateSwitchChannel(channelUID, newState.tampered); - break; - case CHANNEL_VIBRATION: - updateSwitchChannel(channelUID, newState.vibration); - break; - case CHANNEL_CARBONMONOXIDE: - updateSwitchChannel(channelUID, newState.carbonmonoxide); - break; - case CHANNEL_AIRQUALITY: - updateStringChannel(channelUID, newState.airquality); - break; - case CHANNEL_AIRQUALITYPPB: - updateDecimalTypeChannel(channelUID, newState.airqualityppb); - break; - case CHANNEL_BUTTON: - updateDecimalTypeChannel(channelUID, newState.buttonevent); - break; - case CHANNEL_BUTTONEVENT: + } + case CHANNEL_WATERLEAKAGE -> updateSwitchChannel(channelUID, newState.water); + case CHANNEL_FIRE -> updateSwitchChannel(channelUID, newState.fire); + case CHANNEL_ALARM -> updateSwitchChannel(channelUID, newState.alarm); + case CHANNEL_TAMPERED -> updateSwitchChannel(channelUID, newState.tampered); + case CHANNEL_VIBRATION -> updateSwitchChannel(channelUID, newState.vibration); + case CHANNEL_CARBONMONOXIDE -> updateSwitchChannel(channelUID, newState.carbonmonoxide); + case CHANNEL_AIRQUALITY -> updateStringChannel(channelUID, newState.airquality); + case CHANNEL_AIRQUALITYPPB -> updateQuantityTypeChannel(channelUID, newState.airqualityppb, + PARTS_PER_BILLION); + case CHANNEL_MOISTURE -> updateQuantityTypeChannel(channelUID, newState.moisture, PERCENT); + case CHANNEL_BUTTON -> updateDecimalTypeChannel(channelUID, newState.buttonevent); + case CHANNEL_BUTTONEVENT -> { Integer buttonevent = newState.buttonevent; if (buttonevent != null && !initializing) { triggerChannel(channelUID, String.valueOf(buttonevent)); } - break; - case CHANNEL_GESTURE: - updateDecimalTypeChannel(channelUID, newState.gesture); - break; - case CHANNEL_GESTUREEVENT: + } + case CHANNEL_GESTURE -> updateDecimalTypeChannel(channelUID, newState.gesture); + case CHANNEL_GESTUREEVENT -> { Integer gesture = newState.gesture; if (gesture != null && !initializing) { triggerChannel(channelUID, String.valueOf(gesture)); } - break; + } } } @Override - protected void createTypeSpecificChannels(SensorConfig sensorConfig, SensorState sensorState) { + protected boolean createTypeSpecificChannels(ThingBuilder thingBuilder, SensorConfig sensorConfig, + SensorState sensorState) { + boolean thingEdited = false; + // some Xiaomi sensors - if (sensorConfig.temperature != null) { - createChannel(CHANNEL_TEMPERATURE, ChannelKind.STATE); + if (sensorConfig.temperature != null && createChannel(thingBuilder, CHANNEL_TEMPERATURE, ChannelKind.STATE)) { + thingEdited = true; } // ZHAPresence - e.g. IKEA TRÅDFRI motion sensor - if (sensorState.dark != null) { - createChannel(CHANNEL_DARK, ChannelKind.STATE); + if (sensorState.dark != null && createChannel(thingBuilder, CHANNEL_DARK, ChannelKind.STATE)) { + thingEdited = true; } // ZHAConsumption - e.g Bitron 902010/25 or Heiman SmartPlug - if (sensorState.power != null) { - createChannel(CHANNEL_POWER, ChannelKind.STATE); + if (sensorState.power != null && createChannel(thingBuilder, CHANNEL_POWER, ChannelKind.STATE)) { + thingEdited = true; + } + // ZHAConsumption - e.g. Linky devices second channel + if (sensorState.consumption2 != null && createChannel(thingBuilder, CHANNEL_CONSUMPTION_2, ChannelKind.STATE)) { + thingEdited = true; } // ZHAPower - e.g. Heiman SmartPlug - if (sensorState.voltage != null) { - createChannel(CHANNEL_VOLTAGE, ChannelKind.STATE); + if (sensorState.voltage != null && createChannel(thingBuilder, CHANNEL_VOLTAGE, ChannelKind.STATE)) { + thingEdited = true; } - if (sensorState.current != null) { - createChannel(CHANNEL_CURRENT, ChannelKind.STATE); + if (sensorState.current != null && createChannel(thingBuilder, CHANNEL_CURRENT, ChannelKind.STATE)) { + thingEdited = true; } // IAS Zone sensor - e.g. Heiman HS1MS motion sensor - if (sensorState.tampered != null) { - createChannel(CHANNEL_TAMPERED, ChannelKind.STATE); + if (sensorState.tampered != null && createChannel(thingBuilder, CHANNEL_TAMPERED, ChannelKind.STATE)) { + thingEdited = true; } // e.g. Aqara Cube - if (sensorState.gesture != null) { - createChannel(CHANNEL_GESTURE, ChannelKind.STATE); - createChannel(CHANNEL_GESTUREEVENT, ChannelKind.TRIGGER); + if (sensorState.gesture != null && (createChannel(thingBuilder, CHANNEL_GESTURE, ChannelKind.STATE) + || createChannel(thingBuilder, CHANNEL_GESTUREEVENT, ChannelKind.TRIGGER))) { + thingEdited = true; } + + return thingEdited; } @Override diff --git a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/netutils/AsyncHttpClient.java b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/netutils/AsyncHttpClient.java index 5ca7c68e6..e5c2fa6fd 100644 --- a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/netutils/AsyncHttpClient.java +++ b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/netutils/AsyncHttpClient.java @@ -48,7 +48,7 @@ public class AsyncHttpClient { * @param timeout A timeout * @return The result */ - public CompletableFuture post(String address, String jsonString, int timeout) { + public CompletableFuture post(String address, @Nullable String jsonString, int timeout) { return doNetwork(HttpMethod.POST, address, jsonString, timeout); } @@ -101,15 +101,16 @@ public class AsyncHttpClient { } request.method(method).timeout(timeout, TimeUnit.MILLISECONDS).send(new BufferingResponseListener() { - @NonNullByDefault({}) + @Override - public void onComplete(org.eclipse.jetty.client.api.Result result) { + public void onComplete(@NonNullByDefault({}) org.eclipse.jetty.client.api.Result result) { final HttpResponse response = (HttpResponse) result.getResponse(); if (result.getFailure() != null) { f.completeExceptionally(result.getFailure()); return; } - f.complete(new Result(getContentAsString(), response.getStatus())); + String content = getContentAsString(); + f.complete(new Result(content != null ? content : "", response.getStatus())); } }); return f; 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 f2c3d4e4c..9b31fa2d3 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 @@ -16,6 +16,9 @@ import java.net.URI; import java.util.Map; import java.util.Objects; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import org.eclipse.jdt.annotation.NonNullByDefault; @@ -29,6 +32,7 @@ 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.types.ResourceType; +import org.openhab.core.common.ThreadPoolManager; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -46,23 +50,33 @@ import com.google.gson.Gson; public class WebSocketConnection { private static final AtomicInteger INSTANCE_COUNTER = new AtomicInteger(); private final Logger logger = LoggerFactory.getLogger(WebSocketConnection.class); + private final ScheduledExecutorService scheduler = ThreadPoolManager.getScheduledPool("thingHandler"); private final WebSocketClient client; private final String socketName; private final Gson gson; + private int watchdogInterval; private final WebSocketConnectionListener connectionListener; private final Map listeners = new ConcurrentHashMap<>(); private ConnectionState connectionState = ConnectionState.DISCONNECTED; + private @Nullable ScheduledFuture watchdogJob; + private @Nullable Session session; - public WebSocketConnection(WebSocketConnectionListener listener, WebSocketClient client, Gson gson) { + public WebSocketConnection(WebSocketConnectionListener listener, WebSocketClient client, Gson gson, + int watchdogInterval) { this.connectionListener = listener; this.client = client; this.client.setMaxIdleTimeout(0); this.gson = gson; this.socketName = "Websocket$" + System.currentTimeMillis() + "-" + INSTANCE_COUNTER.incrementAndGet(); + this.watchdogInterval = watchdogInterval; + } + + public void setWatchdogInterval(int watchdogInterval) { + this.watchdogInterval = watchdogInterval; } public void start(String ip) { @@ -73,18 +87,47 @@ public class WebSocketConnection { return; } else if (connectionState == ConnectionState.DISCONNECTING) { logger.warn("{} trying to re-connect while still disconnecting", socketName); + return; } try { + connectionState = ConnectionState.CONNECTING; URI destUri = URI.create("ws://" + ip); client.start(); logger.debug("Trying to connect {} to {}", socketName, destUri); client.connect(this, destUri).get(); } catch (Exception e) { - connectionListener.connectionLost("Error while connecting: " + e.getMessage()); + String reason = "Error while connecting: " + e.getMessage(); + if (e.getMessage() == null) { + logger.warn("{}: {}", socketName, reason, e); + } else { + logger.warn("{}: {}", socketName, reason); + } + connectionListener.webSocketConnectionLost(reason); } } - public void close() { + private void startOrResetWatchdogTimer() { + stopWatchdogTimer(); // stop already running timer + watchdogJob = scheduler.schedule( + () -> connectionListener.webSocketConnectionLost( + "Watchdog timed out after " + watchdogInterval + "s. Websocket seems to be dead."), + watchdogInterval, TimeUnit.SECONDS); + } + + private void stopWatchdogTimer() { + ScheduledFuture watchdogTimer = this.watchdogJob; + if (watchdogTimer != null) { + watchdogTimer.cancel(false); + this.watchdogJob = null; + } + } + + /** + * dispose the websocket (close connection and destroy client) + * + */ + public void dispose() { + stopWatchdogTimer(); try { connectionState = ConnectionState.DISCONNECTING; client.stop(); @@ -92,6 +135,7 @@ public class WebSocketConnection { logger.debug("{} encountered an error while closing connection", socketName, e); } client.destroy(); + connectionState = ConnectionState.DISCONNECTED; } public void registerListener(ResourceType resourceType, String sensorID, WebSocketMessageListener listener) { @@ -108,17 +152,19 @@ public class WebSocketConnection { connectionState = ConnectionState.CONNECTED; logger.debug("{} successfully connected to {}: {}", socketName, session.getRemoteAddress().getAddress(), session.hashCode()); - connectionListener.connectionEstablished(); + connectionListener.webSocketConnectionEstablished(); + startOrResetWatchdogTimer(); this.session = session; } - @SuppressWarnings({ "null", "unused" }) + @SuppressWarnings("unused") @OnWebSocketMessage public void onMessage(Session session, String message) { if (!session.equals(this.session)) { handleWrongSession(session, message); return; } + startOrResetWatchdogTimer(); logger.trace("{} received raw data: {}", socketName, message); try { @@ -128,7 +174,16 @@ public class WebSocketConnection { return; } - WebSocketMessageListener listener = listeners.get(getListenerId(changedMessage.r, changedMessage.id)); + ResourceType resourceType = changedMessage.r; + String resourceId = changedMessage.id; + + if (resourceType == ResourceType.SCENES) { + // scene recalls + resourceType = ResourceType.GROUPS; + resourceId = changedMessage.gid; + } + + WebSocketMessageListener listener = listeners.get(getListenerId(resourceType, resourceId)); if (listener == null) { logger.trace( "Couldn't find listener for id {} with resource type {}. Either no thing for this id has been defined or this is a bug.", @@ -136,6 +191,7 @@ public class WebSocketConnection { return; } + // we still need the original resource type here Class expectedMessageType = changedMessage.r.getExpectedMessageType(); if (expectedMessageType == null) { logger.warn( @@ -144,11 +200,8 @@ public class WebSocketConnection { return; } - DeconzBaseMessage deconzMessage = gson.fromJson(message, expectedMessageType); - if (deconzMessage != null) { - listener.messageReceived(changedMessage.id, deconzMessage); - - } + DeconzBaseMessage deconzMessage = Objects.requireNonNull(gson.fromJson(message, expectedMessageType)); + listener.messageReceived(deconzMessage); } catch (RuntimeException e) { // we need to catch all processing exceptions, otherwise they could affect the connection logger.warn("{} encountered an error while processing the message {}: {}", socketName, message, @@ -159,17 +212,13 @@ public class WebSocketConnection { @SuppressWarnings("unused") @OnWebSocketError public void onError(@Nullable Session session, Throwable cause) { - if (session == null) { - logger.trace("Encountered an error while processing on error without session. Connection state is {}: {}", - connectionState, cause.getMessage()); - return; - } - if (!session.equals(this.session)) { + if (session != null && !session.equals(this.session)) { handleWrongSession(session, "Connection error: " + cause.getMessage()); return; } logger.warn("{} connection errored, closing: {}", socketName, cause.getMessage()); + stopWatchdogTimer(); Session storedSession = this.session; if (storedSession != null && storedSession.isOpen()) { storedSession.close(-1, "Processing error"); @@ -185,12 +234,13 @@ public class WebSocketConnection { } logger.trace("{} closed connection: {} / {}", socketName, statusCode, reason); connectionState = ConnectionState.DISCONNECTED; + stopWatchdogTimer(); this.session = null; - connectionListener.connectionLost(reason); + connectionListener.webSocketConnectionLost(reason); } private void handleWrongSession(Session session, String message) { - logger.warn("{}/{} received and discarded message for other session {}: {}.", socketName, session.hashCode(), + logger.warn("{}{} received and discarded message for other or session {}: {}.", socketName, session.hashCode(), session.hashCode(), message); if (session.isOpen()) { // Close the session if it is still open. It should already be closed anyway diff --git a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/netutils/WebSocketConnectionListener.java b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/netutils/WebSocketConnectionListener.java index a17c08470..44bfbfedf 100644 --- a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/netutils/WebSocketConnectionListener.java +++ b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/netutils/WebSocketConnectionListener.java @@ -24,12 +24,12 @@ public interface WebSocketConnectionListener { /** * Connection successfully established. */ - void connectionEstablished(); + void webSocketConnectionEstablished(); /** * Connection lost. A reconnect timer has been started. * * @param reason A reason for the disconnection */ - void connectionLost(String reason); + void webSocketConnectionLost(String reason); } 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 787a46ba8..2befa091f 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 @@ -25,8 +25,7 @@ public interface WebSocketMessageListener { /** * A new message was received * - * @param sensorID The sensor ID (API endpoint) * @param message The received message */ - void messageReceived(String sensorID, DeconzBaseMessage message); + void messageReceived(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 index a8a4962ec..e38312823 100644 --- 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 @@ -21,20 +21,24 @@ 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} + * 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"), + LUMINAIRE("Luminaire"), + ROOM("Room"), + LIGHT_SOURCE("Lightsource"), 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; + private final String type; GroupType(String type) { this.type = type; diff --git a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/types/LightType.java b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/types/LightType.java index fb2be5f56..d611b2fab 100644 --- a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/types/LightType.java +++ b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/types/LightType.java @@ -21,7 +21,8 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** - * Type of a light as reported by the REST API for usage in {@link org.openhab.binding.deconz.internal.dto.LightMessage} + * Type of a light as reported by the REST API for usage in + * {@link org.openhab.binding.deconz.internal.dto.LightMessage} * * @author Jan N. Klug - Initial contribution */ @@ -46,7 +47,7 @@ public enum LightType { .collect(Collectors.toMap(v -> v.type, v -> v)); private static final Logger LOGGER = LoggerFactory.getLogger(LightType.class); - private String type; + private final String type; LightType(String type) { this.type = type; 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 index 6c159a7e7..7059c0b26 100644 --- 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 @@ -35,14 +35,15 @@ public enum ResourceType { GROUPS("groups", "action", GroupMessage.class), LIGHTS("lights", "state", LightMessage.class), SENSORS("sensors", "config", SensorMessage.class), + SCENES("scenes", "", DeconzBaseMessage.class), UNKNOWN("", "", null); 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; + private final String identifier; + private final String commandUrl; private @Nullable Class expectedMessageType; ResourceType(String identifier, String commandUrl, diff --git a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/types/ThermostatMode.java b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/types/ThermostatMode.java index de3cf74ac..d38596654 100644 --- a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/types/ThermostatMode.java +++ b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/types/ThermostatMode.java @@ -21,7 +21,8 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** - * Thermostat mode as reported by the REST API for usage in {@link org.openhab.binding.deconz.internal.dto.SensorConfig} + * Thermostat mode as reported by the REST API for usage in + * {@link org.openhab.binding.deconz.internal.dto.SensorConfig} * * @author Lukas Agethen - Initial contribution */ @@ -36,7 +37,7 @@ public enum ThermostatMode { .collect(Collectors.toMap(v -> v.deconzValue, v -> v)); private static final Logger LOGGER = LoggerFactory.getLogger(ThermostatMode.class); - private String deconzValue; + private final String deconzValue; ThermostatMode(String deconzValue) { this.deconzValue = deconzValue; diff --git a/bundles/org.openhab.binding.deconz/src/main/resources/OH-INF/config/config.xml b/bundles/org.openhab.binding.deconz/src/main/resources/OH-INF/config/config.xml index bd0b4fab2..c9ac5f7fc 100644 --- a/bundles/org.openhab.binding.deconz/src/main/resources/OH-INF/config/config.xml +++ b/bundles/org.openhab.binding.deconz/src/main/resources/OH-INF/config/config.xml @@ -5,33 +5,68 @@ xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0 https://openhab.org/schemas/config-description-1.0.0.xsd"> + + + true + + + + true + network-address IP address or host name of deCONZ interface. - - - Port of the deCONZ HTTP interface. - 80 - - - - Port of the deCONZ Websocket. - true - password If no API Key is provided, a new one will be requested. You need to authorize the access on the deCONZ web interface. - + + + Port of the deCONZ HTTP interface. + true + 80 + + Timeout for asynchronous HTTP requests (in milliseconds). true 2000 + + + Port of the deCONZ Websocket. + true + + + + Timeout for the websocket connection (in seconds). + true + 120 + + + + + + + The deCONZ bridge assigns an integer number ID to each group. + + + + Time to move between two states. If empty, the default of the group is used. Resolution is 1/10 second. + + + + Override the default color mode (auto-detect) + + + + + true + @@ -56,6 +91,12 @@ Time to move between two states. If empty, the default of the device is used. Resolution is 1/10 second. + + + Interval to poll the deCONZ Gateway for this light's "last_seen" channel. Polling is disabled when set + to 0 (default: 1440, once per day). + 1440 + @@ -76,21 +117,12 @@ true + + + Interval to poll the deCONZ Gateway for this light's "last_seen" channel. Polling is disabled when set + to 0 (default: 1440, once per day). + 1440 + - - - - The deCONZ bridge assigns an integer number ID to each group. - - - - Override the default color mode (auto-detect) - - - - - true - - diff --git a/bundles/org.openhab.binding.deconz/src/main/resources/OH-INF/i18n/deconz.properties b/bundles/org.openhab.binding.deconz/src/main/resources/OH-INF/i18n/deconz.properties index c2e8345ec..5f507c1bb 100644 --- a/bundles/org.openhab.binding.deconz/src/main/resources/OH-INF/i18n/deconz.properties +++ b/bundles/org.openhab.binding.deconz/src/main/resources/OH-INF/i18n/deconz.properties @@ -5,59 +5,42 @@ addon.deconz.description = Allows to use the real-time channel of the deCONZ sof # thing types +thing-type.deconz.airqualitysensor.label = Carbon-monoxide Sensor thing-type.deconz.alarmsensor.label = Alarm Sensor -thing-type.deconz.alarmsensor.description = An alarm sensor thing-type.deconz.batterysensor.label = Battery Sensor -thing-type.deconz.batterysensor.description = A battery sensor thing-type.deconz.carbonmonoxidesensor.label = Carbon-monoxide Sensor -thing-type.deconz.airqualitysensor.label = Air quality Sensor -thing-type.deconz.airqualitysensor.description = An air quality sensor thing-type.deconz.colorcontrol.label = Color Controller thing-type.deconz.colorlight.label = Color Light thing-type.deconz.colorlight.description = A dimmable light with adjustable color. thing-type.deconz.colortemperaturelight.label = Color-Temperature Light thing-type.deconz.colortemperaturelight.description = A dimmable light with adjustable color temperature. thing-type.deconz.consumptionsensor.label = Consumption Sensor -thing-type.deconz.consumptionsensor.description = A consumption sensor thing-type.deconz.daylightsensor.label = Daylight Sensor -thing-type.deconz.daylightsensor.description = A daylight sensor thing-type.deconz.deconz.label = deCONZ thing-type.deconz.deconz.description = A running deCONZ software instance. thing-type.deconz.dimmablelight.label = Dimmable Light -thing-type.deconz.dimmablelight.description = A dimmable light. thing-type.deconz.doorlock.label = Doorlock thing-type.deconz.doorlock.description = A doorlock that can be locked (ON) or unlocked (OFF). thing-type.deconz.extendedcolorlight.label = Color Light thing-type.deconz.extendedcolorlight.description = A dimmable light with adjustable color. thing-type.deconz.firesensor.label = Fire Sensor -thing-type.deconz.firesensor.description = A fire sensor thing-type.deconz.humiditysensor.label = Humidity Sensor -thing-type.deconz.humiditysensor.description = A humidity sensor thing-type.deconz.lightgroup.label = Light Group thing-type.deconz.lightsensor.label = Light Sensor -thing-type.deconz.lightsensor.description = A light sensor +thing-type.deconz.moisturesensor.label = Moisture Sensor thing-type.deconz.onofflight.label = On/Off Light thing-type.deconz.onofflight.description = A light that can be turned on or off. thing-type.deconz.openclosesensor.label = Open/Close Sensor -thing-type.deconz.openclosesensor.description = An open/close sensor thing-type.deconz.powersensor.label = Power Sensor -thing-type.deconz.powersensor.description = A power sensor thing-type.deconz.presencesensor.label = Presence Sensor -thing-type.deconz.presencesensor.description = A Presence sensor thing-type.deconz.pressuresensor.label = Pressure Sensor -thing-type.deconz.pressuresensor.description = A pressure senor thing-type.deconz.switch.label = Switch/Button -thing-type.deconz.switch.description = A switch or button thing-type.deconz.temperaturesensor.label = Temperature Sensor -thing-type.deconz.temperaturesensor.description = A temperature sensor thing-type.deconz.thermostat.label = Thermostat thing-type.deconz.thermostat.description = A Thermostat sensor/actor thing-type.deconz.vibrationsensor.label = Vibration Sensor -thing-type.deconz.vibrationsensor.description = A vibration sensor thing-type.deconz.warningdevice.label = Warning Device -thing-type.deconz.warningdevice.description = A warning device thing-type.deconz.waterleakagesensor.label = Water Leakage Sensor -thing-type.deconz.waterleakagesensor.description = A water leakage sensor thing-type.deconz.windowcovering.label = Window Covering thing-type.deconz.windowcovering.description = A device to cover windows. @@ -65,24 +48,32 @@ thing-type.deconz.windowcovering.description = A device to cover windows. thing-type.config.deconz.bridge.apikey.label = API Key thing-type.config.deconz.bridge.apikey.description = If no API Key is provided, a new one will be requested. You need to authorize the access on the deCONZ web interface. +thing-type.config.deconz.bridge.group.http.label = HTTP Connection +thing-type.config.deconz.bridge.group.websocket.label = Websocket Connection thing-type.config.deconz.bridge.host.label = Host Address thing-type.config.deconz.bridge.host.description = IP address or host name of deCONZ interface. -thing-type.config.deconz.bridge.httpPort.label = HTTP Port +thing-type.config.deconz.bridge.httpPort.label = Port thing-type.config.deconz.bridge.httpPort.description = Port of the deCONZ HTTP interface. -thing-type.config.deconz.bridge.port.label = Websocket Port +thing-type.config.deconz.bridge.port.label = Port thing-type.config.deconz.bridge.port.description = Port of the deCONZ Websocket. thing-type.config.deconz.bridge.timeout.label = Timeout thing-type.config.deconz.bridge.timeout.description = Timeout for asynchronous HTTP requests (in milliseconds). +thing-type.config.deconz.bridge.websocketTimeout.label = Timeout +thing-type.config.deconz.bridge.websocketTimeout.description = Timeout for the websocket connection (in seconds). thing-type.config.deconz.colorlight.colormode.label = Color Mode thing-type.config.deconz.colorlight.colormode.description = Override the default color mode (auto-detect) thing-type.config.deconz.colorlight.colormode.option.hs = HSB thing-type.config.deconz.colorlight.colormode.option.xy = XY thing-type.config.deconz.colorlight.id.label = Device ID thing-type.config.deconz.colorlight.id.description = The deCONZ bridge assigns an integer number ID to each device. +thing-type.config.deconz.colorlight.lastSeenPolling.label = LastSeen Poll Interval +thing-type.config.deconz.colorlight.lastSeenPolling.description = Interval to poll the deCONZ Gateway for this light's "last_seen" channel. Polling is disabled when set to 0 (default: 1440, once per day). thing-type.config.deconz.colorlight.transitiontime.label = Transition Time thing-type.config.deconz.colorlight.transitiontime.description = Time to move between two states. If empty, the default of the device is used. Resolution is 1/10 second. thing-type.config.deconz.light.id.label = Device ID thing-type.config.deconz.light.id.description = The deCONZ bridge assigns an integer number ID to each device. +thing-type.config.deconz.light.lastSeenPolling.label = LastSeen Poll Interval +thing-type.config.deconz.light.lastSeenPolling.description = Interval to poll the deCONZ Gateway for this light's "last_seen" channel. Polling is disabled when set to 0 (default: 1440, once per day). thing-type.config.deconz.light.transitiontime.label = Transition Time thing-type.config.deconz.light.transitiontime.description = Time to move between two states. If empty, the default of the device is used. Resolution is 1/10 second. thing-type.config.deconz.lightgroup.colormode.label = Color Mode @@ -91,6 +82,8 @@ thing-type.config.deconz.lightgroup.colormode.option.hs = HSB thing-type.config.deconz.lightgroup.colormode.option.xy = XY thing-type.config.deconz.lightgroup.id.label = Device ID thing-type.config.deconz.lightgroup.id.description = The deCONZ bridge assigns an integer number ID to each group. +thing-type.config.deconz.lightgroup.transitiontime.label = Transition Time +thing-type.config.deconz.lightgroup.transitiontime.description = Time to move between two states. If empty, the default of the group is used. Resolution is 1/10 second. thing-type.config.deconz.sensor.id.label = Device ID thing-type.config.deconz.sensor.id.description = The deCONZ bridge assigns an integer number ID to each device. thing-type.config.deconz.sensor.lastSeenPolling.label = LastSeen Poll Interval @@ -98,6 +91,10 @@ thing-type.config.deconz.sensor.lastSeenPolling.description = Interval to poll t # channel types +channel-type.deconz.airquality.label = Air Quality +channel-type.deconz.airquality.description = Current air quality level based on volatile organic compounds (VOCs) measurement. Example: good or poor, ... +channel-type.deconz.airqualityppb.label = Air Quality (ppb) +channel-type.deconz.airqualityppb.description = Current air quality based on measurements of volatile organic compounds (VOCs). The measured value is specified in ppb (parts per billion). channel-type.deconz.alarm.label = Alarm channel-type.deconz.alarm.description = Alarm was triggered. channel-type.deconz.alert.label = Alert @@ -114,10 +111,6 @@ channel-type.deconz.buttonevent.label = Button Trigger channel-type.deconz.buttonevent.description = This channel is triggered on a button event. The trigger payload consists of the button event number. channel-type.deconz.carbonmonoxide.label = Carbon-monoxide channel-type.deconz.carbonmonoxide.description = Carbon-monoxide was detected. -channel-type.deconz.airquality.label = Air quality level -channel-type.deconz.airquality.description = Current air quality level based on volatile organic compounds (VOCs) measurement. Example: good or poor, ... -channel-type.deconz.airqualityppb.label = Air quality in ppb -channel-type.deconz.airqualityppb.description = Current air quality based on measurements of volatile organic compounds (VOCs). The measured value is specified in ppb (parts per billion). channel-type.deconz.consumption.label = Consumption channel-type.deconz.consumption.description = Current consumption channel-type.deconz.ct.label = Color Temperature @@ -130,6 +123,7 @@ channel-type.deconz.daylight.label = Daylight channel-type.deconz.daylight.description = Light level is above the daylight threshold. channel-type.deconz.effect.label = Effect Channel channel-type.deconz.effectSpeed.label = Effect Speed Channel +channel-type.deconz.externalwindowopen.label = External Window Open channel-type.deconz.fire.label = Fire channel-type.deconz.fire.description = A fire was detected. channel-type.deconz.gesture.label = Gesture @@ -145,7 +139,7 @@ channel-type.deconz.gesture.state.option.7 = Rotate Clockwise channel-type.deconz.gesture.state.option.8 = Rotate Counter Clockwise channel-type.deconz.gestureevent.label = Gesture Trigger channel-type.deconz.gestureevent.description = This channel is triggered on a gesture event. The trigger payload consists of the gesture event number. -channel-type.deconz.heatsetpoint.label = Target Temperature +channel-type.deconz.heatsetpoint.label = Target temperature channel-type.deconz.heatsetpoint.description = Target temperature channel-type.deconz.humidity.label = Humidity channel-type.deconz.humidity.description = Current humidity @@ -156,7 +150,6 @@ channel-type.deconz.last_updated.label = Last Updated channel-type.deconz.last_updated.description = The date and time when the sensor was last updated. channel-type.deconz.last_updated.state.pattern = %1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS channel-type.deconz.light.label = Lightlevel -channel-type.deconz.light.description = A light level channel-type.deconz.light.state.option.daylight = Daylight channel-type.deconz.light.state.option.sunset = Sunset channel-type.deconz.light.state.option.dark = Dark @@ -165,11 +158,15 @@ channel-type.deconz.light_level.description = Current light level. channel-type.deconz.lightlux.label = Illuminance channel-type.deconz.lightlux.description = Current light illuminance channel-type.deconz.lock.label = Lock +channel-type.deconz.locked.label = Locked +channel-type.deconz.locked.description = Status of this thermostat's child lock. channel-type.deconz.mode.label = Mode channel-type.deconz.mode.description = Current mode channel-type.deconz.mode.state.option.AUTO = auto channel-type.deconz.mode.state.option.HEAT = heat channel-type.deconz.mode.state.option.OFF = off +channel-type.deconz.moisture.label = Moisture +channel-type.deconz.moisture.description = Current moisture channel-type.deconz.offset.label = Offset channel-type.deconz.offset.description = Temperature offset channel-type.deconz.ontime.label = On Time @@ -196,8 +193,28 @@ channel-type.deconz.voltage.label = Voltage channel-type.deconz.voltage.description = Current voltage channel-type.deconz.waterleakage.label = Water Leakage channel-type.deconz.waterleakage.description = Water leakage detected +channel-type.deconz.windowopen.label = Window Open # thing status descriptions offline.light-not-reachable = Not reachable offline.sensor-not-reachable = Not reachable + +# actions + +action.permit-join-network.duration.label = Duration +action.permit-join-network.duration.description = Number of seconds to allow new devices to join. +action.permit-join-network.label = permit join Zigbee network +action.permit-join-network.description = Permits new devices to join the Zigbee network for a given duration (default 120s). +action.create-scene.label = create a scene +action.create-scene.description = Creates a new scene and returns the new scene's id. +action.create-scene.name.label = Name +action.create-scene.name.description = Name of the scene to create. +action.delete-scene.label = delete a scene +action.delete-scene.description = Deletes a scene. +action.delete-scene.sceneId.label = Scene id +action.delete-scene.sceneId.description = Id of the scene to delete. +action.store-as-scene.label = store as scene +action.store-as-scene.description = Stores the current light state as scene +action.store-as-scene.sceneId.label = Scene id +action.store-as-scene.sceneId.description = Id of the scene to store current group's state as. 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 index 80be02746..df389ed9e 100644 --- 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 @@ -19,6 +19,10 @@ + + 1 + + uid @@ -28,6 +32,9 @@ Switch "On" if all lights in this group are "On", otherwise "Off". + + Lighting + @@ -35,12 +42,18 @@ Switch "On" if any light in this group is "On", otherwise "Off". + + Lighting + String + + Lighting + 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 fa134f4a7..7130c6148 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 @@ -9,10 +9,9 @@ - A warning device Siren - + uid @@ -40,13 +39,19 @@ + A light that can be turned on or off. + + + 1 + + uid @@ -57,14 +62,17 @@ - A dimmable light. Lightbulb - + + + 1 + + uid @@ -81,9 +89,13 @@ - + + + 1 + + uid @@ -99,9 +111,13 @@ - + + + 1 + + uid @@ -118,9 +134,13 @@ - + + + 1 + + uid @@ -161,16 +181,23 @@ Number:Time Time that the light stays on before switched off automatically (0=forever) + String + + Lighting + Number + + Lighting + diff --git a/bundles/org.openhab.binding.deconz/src/main/resources/OH-INF/thing/sensor-thing-types.xml b/bundles/org.openhab.binding.deconz/src/main/resources/OH-INF/thing/sensor-thing-types.xml index 07b59216d..60ab11122 100644 --- a/bundles/org.openhab.binding.deconz/src/main/resources/OH-INF/thing/sensor-thing-types.xml +++ b/bundles/org.openhab.binding.deconz/src/main/resources/OH-INF/thing/sensor-thing-types.xml @@ -9,13 +9,16 @@ - A Presence sensor + + 1 + + uid @@ -42,7 +45,6 @@ - A power sensor @@ -58,7 +60,7 @@ Current power usage Energy - + @@ -82,10 +84,9 @@ - A consumption sensor - - + + uid @@ -98,7 +99,7 @@ Current consumption Energy - + @@ -113,6 +114,10 @@ + + 1 + + uid @@ -123,7 +128,6 @@ - A switch or button @@ -147,7 +151,7 @@ Number The Button that was last pressed on the switch. - + @@ -181,7 +185,6 @@ - A light sensor @@ -199,7 +202,7 @@ Number:Illuminance Current light illuminance - + @@ -228,7 +231,6 @@ - A temperature sensor @@ -244,7 +246,7 @@ Current temperature Temperature - + @@ -252,7 +254,6 @@ - A humidity sensor @@ -268,7 +269,7 @@ Current humidity Humidity - + @@ -276,10 +277,9 @@ - A pressure senor - - + + uid @@ -292,7 +292,7 @@ Current pressure Pressure - + @@ -300,10 +300,9 @@ - A daylight sensor - - + + uid @@ -315,13 +314,12 @@ Number Dawn is around 130, sunrise at 140, sunset at 190, and dusk at 210 - + String - A light level @@ -336,7 +334,6 @@ - An open/close sensor @@ -351,7 +348,7 @@ Contact Open/Close detected - + @@ -359,7 +356,6 @@ - A water leakage sensor @@ -382,7 +378,6 @@ - A fire sensor @@ -405,7 +400,6 @@ - An alarm sensor @@ -435,7 +429,6 @@ - A vibration sensor @@ -458,12 +451,15 @@ - A battery sensor + + 1 + + uid @@ -491,13 +487,11 @@ - - - An air quality sensor + @@ -511,20 +505,41 @@ String - + Current air quality level based on volatile organic compounds (VOCs) measurement. Example: good or poor, ... - + Number:Dimensionless - + Current air quality based on measurements of volatile organic compounds (VOCs). The measured value is specified in ppb (parts per billion). - + + + + + + + + + + + + uid + + + + + + Number:Dimensionless + + Current moisture + + @@ -544,12 +559,26 @@ + + Switch + + Status of this thermostat's child lock. + Lock + + + Contact + + + + Contact + + Number:Temperature Target temperature Heating - + String @@ -568,13 +597,13 @@ Number:Temperature Temperature offset - + Number:Dimensionless Current valve position - + diff --git a/bundles/org.openhab.binding.deconz/src/main/resources/OH-INF/update/update.xml b/bundles/org.openhab.binding.deconz/src/main/resources/OH-INF/update/update.xml new file mode 100644 index 000000000..e4639b0ad --- /dev/null +++ b/bundles/org.openhab.binding.deconz/src/main/resources/OH-INF/update/update.xml @@ -0,0 +1,81 @@ + + + + + + + system:battery-level + + + + + + + + system:color + + + + + + + + system:color + + + + + + + + system:brightness + + + + + + + + system:brightness + + + + + + + + system:color + + + + + + + + system:color + + + + + + + + system:power + + + + + + + + system:power + + + system:motion + + + + + 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 8a0a512d4..5091411cc 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 @@ -60,7 +60,7 @@ import com.google.gson.GsonBuilder; * @author Jan N. Klug - Initial contribution */ @ExtendWith(MockitoExtension.class) -@MockitoSettings(strictness = Strictness.LENIENT) +@MockitoSettings(strictness = Strictness.WARN) @NonNullByDefault public class DeconzTest { private @NonNullByDefault({}) Gson gson; diff --git a/bundles/org.openhab.binding.deconz/src/test/java/org/openhab/binding/deconz/LightGroupTest.java b/bundles/org.openhab.binding.deconz/src/test/java/org/openhab/binding/deconz/LightGroupTest.java new file mode 100644 index 000000000..2a6ca1ead --- /dev/null +++ b/bundles/org.openhab.binding.deconz/src/test/java/org/openhab/binding/deconz/LightGroupTest.java @@ -0,0 +1,89 @@ +/** + * Copyright (c) 2010-2023 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; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.ArgumentMatchers.eq; +import static org.openhab.binding.deconz.internal.BindingConstants.CHANNEL_ALL_ON; +import static org.openhab.binding.deconz.internal.BindingConstants.CHANNEL_ANY_ON; +import static org.openhab.binding.deconz.internal.BindingConstants.THING_TYPE_LIGHTGROUP; +import static org.openhab.core.thing.internal.ThingManagerImpl.PROPERTY_THING_TYPE_VERSION; + +import java.io.IOException; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +import org.openhab.binding.deconz.internal.DeconzDynamicCommandDescriptionProvider; +import org.openhab.binding.deconz.internal.dto.GroupMessage; +import org.openhab.binding.deconz.internal.handler.GroupThingHandler; +import org.openhab.binding.deconz.internal.types.GroupType; +import org.openhab.binding.deconz.internal.types.GroupTypeDeserializer; +import org.openhab.core.library.CoreItemFactory; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingUID; +import org.openhab.core.thing.binding.ThingHandlerCallback; +import org.openhab.core.thing.binding.builder.ChannelBuilder; +import org.openhab.core.thing.binding.builder.ThingBuilder; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + +/** + * This class provides tests for deconz light groups + * + * @author Christoph Weitkamp - Initial contribution + */ +@ExtendWith(MockitoExtension.class) +@NonNullByDefault +public class LightGroupTest { + private @NonNullByDefault({}) Gson gson; + + private @Mock @NonNullByDefault({}) ThingHandlerCallback thingHandlerCallback; + private @Mock @NonNullByDefault({}) DeconzDynamicCommandDescriptionProvider commandDescriptionProvider; + + @BeforeEach + public void initialize() { + GsonBuilder gsonBuilder = new GsonBuilder(); + gsonBuilder.registerTypeAdapter(GroupType.class, new GroupTypeDeserializer()); + gson = gsonBuilder.create(); + } + + @Test + public void lightGroupUpdateTest() throws IOException { + GroupMessage lightMessage = DeconzTest.getObjectFromJson("group.json", GroupMessage.class, gson); + assertNotNull(lightMessage); + + ThingUID thingUID = new ThingUID("deconz", "lightgroup"); + ChannelUID channelUIDAllOn = new ChannelUID(thingUID, CHANNEL_ALL_ON); + ChannelUID channelUIDAnyOn = new ChannelUID(thingUID, CHANNEL_ANY_ON); + + Thing group = ThingBuilder.create(THING_TYPE_LIGHTGROUP, thingUID) + .withProperties(Map.of(PROPERTY_THING_TYPE_VERSION, "1")) + .withChannel(ChannelBuilder.create(channelUIDAllOn, CoreItemFactory.SWITCH).build()) + .withChannel(ChannelBuilder.create(channelUIDAnyOn, CoreItemFactory.SWITCH).build()).build(); + GroupThingHandler groupThingHandler = new GroupThingHandler(group, gson, commandDescriptionProvider); + groupThingHandler.setCallback(thingHandlerCallback); + + groupThingHandler.messageReceived(lightMessage); + Mockito.verify(thingHandlerCallback).stateUpdated(eq(channelUIDAllOn), eq(OnOffType.OFF)); + Mockito.verify(thingHandlerCallback).stateUpdated(eq(channelUIDAnyOn), eq(OnOffType.OFF)); + } +} diff --git a/bundles/org.openhab.binding.deconz/src/test/java/org/openhab/binding/deconz/LightsTest.java b/bundles/org.openhab.binding.deconz/src/test/java/org/openhab/binding/deconz/LightsTest.java index e8a543187..f0e80d6b2 100644 --- a/bundles/org.openhab.binding.deconz/src/test/java/org/openhab/binding/deconz/LightsTest.java +++ b/bundles/org.openhab.binding.deconz/src/test/java/org/openhab/binding/deconz/LightsTest.java @@ -15,6 +15,7 @@ package org.openhab.binding.deconz; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.mockito.ArgumentMatchers.*; import static org.openhab.binding.deconz.internal.BindingConstants.*; +import static org.openhab.core.thing.internal.ThingManagerImpl.PROPERTY_THING_TYPE_VERSION; import java.io.IOException; import java.util.HashMap; @@ -77,34 +78,36 @@ public class LightsTest { assertNotNull(lightMessage); ThingUID thingUID = new ThingUID("deconz", "light"); - ChannelUID channelUID_bri = new ChannelUID(thingUID, CHANNEL_BRIGHTNESS); - ChannelUID channelUID_ct = new ChannelUID(thingUID, CHANNEL_COLOR_TEMPERATURE); + ChannelUID channelUIDBri = new ChannelUID(thingUID, CHANNEL_BRIGHTNESS); + ChannelUID channelUIDCt = new ChannelUID(thingUID, CHANNEL_COLOR_TEMPERATURE); Thing light = ThingBuilder.create(THING_TYPE_COLOR_TEMPERATURE_LIGHT, thingUID) - .withChannel(ChannelBuilder.create(channelUID_bri, "Dimmer").build()) - .withChannel(ChannelBuilder.create(channelUID_ct, "Number").build()).build(); + .withProperties(Map.of(PROPERTY_THING_TYPE_VERSION, "1")) + .withChannel(ChannelBuilder.create(channelUIDBri, "Dimmer").build()) + .withChannel(ChannelBuilder.create(channelUIDCt, "Number").build()).build(); LightThingHandler lightThingHandler = new LightThingHandler(light, gson, stateDescriptionProvider, commandDescriptionProvider); lightThingHandler.setCallback(thingHandlerCallback); - lightThingHandler.messageReceived("", lightMessage); - Mockito.verify(thingHandlerCallback).stateUpdated(eq(channelUID_bri), eq(new PercentType("21"))); - Mockito.verify(thingHandlerCallback).stateUpdated(eq(channelUID_ct), eq(new DecimalType("2500"))); + lightThingHandler.messageReceived(lightMessage); + Mockito.verify(thingHandlerCallback).stateUpdated(eq(channelUIDBri), eq(new PercentType("21"))); + Mockito.verify(thingHandlerCallback).stateUpdated(eq(channelUIDCt), eq(new DecimalType("2500"))); } @Test public void colorTemperatureLightStateDescriptionProviderTest() { ThingUID thingUID = new ThingUID("deconz", "light"); - ChannelUID channelUID_bri = new ChannelUID(thingUID, CHANNEL_BRIGHTNESS); - ChannelUID channelUID_ct = new ChannelUID(thingUID, CHANNEL_COLOR_TEMPERATURE); + ChannelUID channelUIDBri = new ChannelUID(thingUID, CHANNEL_BRIGHTNESS); + ChannelUID channelUIDCt = new ChannelUID(thingUID, CHANNEL_COLOR_TEMPERATURE); Map properties = new HashMap<>(); properties.put(PROPERTY_CT_MAX, "500"); properties.put(PROPERTY_CT_MIN, "200"); + properties.put(PROPERTY_THING_TYPE_VERSION, "1"); Thing light = ThingBuilder.create(THING_TYPE_COLOR_TEMPERATURE_LIGHT, thingUID).withProperties(properties) - .withChannel(ChannelBuilder.create(channelUID_bri, "Dimmer").build()) - .withChannel(ChannelBuilder.create(channelUID_ct, "Number").build()).build(); + .withChannel(ChannelBuilder.create(channelUIDBri, "Dimmer").build()) + .withChannel(ChannelBuilder.create(channelUIDCt, "Number").build()).build(); LightThingHandler lightThingHandler = new LightThingHandler(light, gson, stateDescriptionProvider, commandDescriptionProvider) { // avoid warning when initializing @@ -116,7 +119,7 @@ public class LightsTest { lightThingHandler.initialize(); - Mockito.verify(stateDescriptionProvider).setDescriptionFragment(eq(channelUID_ct), any()); + Mockito.verify(stateDescriptionProvider).setDescriptionFragment(eq(channelUIDCt), any()); } @Test @@ -125,16 +128,17 @@ public class LightsTest { assertNotNull(lightMessage); ThingUID thingUID = new ThingUID("deconz", "light"); - ChannelUID channelUID_bri = new ChannelUID(thingUID, CHANNEL_BRIGHTNESS); + ChannelUID channelUIDBri = new ChannelUID(thingUID, CHANNEL_BRIGHTNESS); Thing light = ThingBuilder.create(THING_TYPE_DIMMABLE_LIGHT, thingUID) - .withChannel(ChannelBuilder.create(channelUID_bri, "Dimmer").build()).build(); + .withProperties(Map.of(PROPERTY_THING_TYPE_VERSION, "1")) + .withChannel(ChannelBuilder.create(channelUIDBri, "Dimmer").build()).build(); LightThingHandler lightThingHandler = new LightThingHandler(light, gson, stateDescriptionProvider, commandDescriptionProvider); lightThingHandler.setCallback(thingHandlerCallback); - lightThingHandler.messageReceived("", lightMessage); - Mockito.verify(thingHandlerCallback).stateUpdated(eq(channelUID_bri), eq(new PercentType("38"))); + lightThingHandler.messageReceived(lightMessage); + Mockito.verify(thingHandlerCallback).stateUpdated(eq(channelUIDBri), eq(new PercentType("38"))); } @Test @@ -143,16 +147,17 @@ public class LightsTest { assertNotNull(lightMessage); ThingUID thingUID = new ThingUID("deconz", "light"); - ChannelUID channelUID_bri = new ChannelUID(thingUID, CHANNEL_BRIGHTNESS); + ChannelUID channelUIDBri = new ChannelUID(thingUID, CHANNEL_BRIGHTNESS); Thing light = ThingBuilder.create(THING_TYPE_DIMMABLE_LIGHT, thingUID) - .withChannel(ChannelBuilder.create(channelUID_bri, "Dimmer").build()).build(); + .withProperties(Map.of(PROPERTY_THING_TYPE_VERSION, "1")) + .withChannel(ChannelBuilder.create(channelUIDBri, "Dimmer").build()).build(); LightThingHandler lightThingHandler = new LightThingHandler(light, gson, stateDescriptionProvider, commandDescriptionProvider); lightThingHandler.setCallback(thingHandlerCallback); - lightThingHandler.messageReceived("", lightMessage); - Mockito.verify(thingHandlerCallback).stateUpdated(eq(channelUID_bri), eq(new PercentType("100"))); + lightThingHandler.messageReceived(lightMessage); + Mockito.verify(thingHandlerCallback).stateUpdated(eq(channelUIDBri), eq(new PercentType("100"))); } @Test @@ -161,16 +166,17 @@ public class LightsTest { assertNotNull(lightMessage); ThingUID thingUID = new ThingUID("deconz", "light"); - ChannelUID channelUID_bri = new ChannelUID(thingUID, CHANNEL_BRIGHTNESS); + ChannelUID channelUIDBri = new ChannelUID(thingUID, CHANNEL_BRIGHTNESS); Thing light = ThingBuilder.create(THING_TYPE_DIMMABLE_LIGHT, thingUID) - .withChannel(ChannelBuilder.create(channelUID_bri, "Dimmer").build()).build(); + .withProperties(Map.of(PROPERTY_THING_TYPE_VERSION, "1")) + .withChannel(ChannelBuilder.create(channelUIDBri, "Dimmer").build()).build(); LightThingHandler lightThingHandler = new LightThingHandler(light, gson, stateDescriptionProvider, commandDescriptionProvider); lightThingHandler.setCallback(thingHandlerCallback); - lightThingHandler.messageReceived("", lightMessage); - Mockito.verify(thingHandlerCallback).stateUpdated(eq(channelUID_bri), eq(new PercentType("0"))); + lightThingHandler.messageReceived(lightMessage); + Mockito.verify(thingHandlerCallback).stateUpdated(eq(channelUIDBri), eq(new PercentType("0"))); } @Test @@ -179,15 +185,16 @@ public class LightsTest { assertNotNull(lightMessage); ThingUID thingUID = new ThingUID("deconz", "light"); - ChannelUID channelUID_pos = new ChannelUID(thingUID, CHANNEL_POSITION); + ChannelUID channelUIDPos = new ChannelUID(thingUID, CHANNEL_POSITION); Thing light = ThingBuilder.create(THING_TYPE_WINDOW_COVERING, thingUID) - .withChannel(ChannelBuilder.create(channelUID_pos, "Rollershutter").build()).build(); + .withProperties(Map.of(PROPERTY_THING_TYPE_VERSION, "1")) + .withChannel(ChannelBuilder.create(channelUIDPos, "Rollershutter").build()).build(); LightThingHandler lightThingHandler = new LightThingHandler(light, gson, stateDescriptionProvider, commandDescriptionProvider); lightThingHandler.setCallback(thingHandlerCallback); - lightThingHandler.messageReceived("", lightMessage); - Mockito.verify(thingHandlerCallback).stateUpdated(eq(channelUID_pos), eq(new PercentType("41"))); + lightThingHandler.messageReceived(lightMessage); + Mockito.verify(thingHandlerCallback).stateUpdated(eq(channelUIDPos), eq(new PercentType("41"))); } } diff --git a/bundles/org.openhab.binding.deconz/src/test/java/org/openhab/binding/deconz/SensorsTest.java b/bundles/org.openhab.binding.deconz/src/test/java/org/openhab/binding/deconz/SensorsTest.java index daefaab40..1c9d51635 100644 --- a/bundles/org.openhab.binding.deconz/src/test/java/org/openhab/binding/deconz/SensorsTest.java +++ b/bundles/org.openhab.binding.deconz/src/test/java/org/openhab/binding/deconz/SensorsTest.java @@ -44,6 +44,7 @@ import org.openhab.core.thing.ThingUID; import org.openhab.core.thing.binding.ThingHandlerCallback; import org.openhab.core.thing.binding.builder.ChannelBuilder; import org.openhab.core.thing.binding.builder.ThingBuilder; +import org.openhab.core.types.UnDefType; import com.google.gson.Gson; import com.google.gson.GsonBuilder; @@ -82,7 +83,7 @@ public class SensorsTest { SensorThingHandler sensorThingHandler = new SensorThingHandler(sensor, gson); sensorThingHandler.setCallback(thingHandlerCallback); - sensorThingHandler.messageReceived("", sensorMessage); + sensorThingHandler.messageReceived(sensorMessage); Mockito.verify(thingHandlerCallback).stateUpdated(eq(channelUID), eq(OnOffType.ON)); } @@ -100,7 +101,7 @@ public class SensorsTest { sensorThingHandler.setCallback(thingHandlerCallback); // ACT - sensorThingHandler.messageReceived("", sensorMessage); + sensorThingHandler.messageReceived(sensorMessage); // ASSERT Mockito.verify(thingHandlerCallback).stateUpdated(eq(channelUID), eq(StringType.valueOf("good"))); @@ -120,10 +121,10 @@ public class SensorsTest { sensorThingHandler.setCallback(thingHandlerCallback); // ACT - sensorThingHandler.messageReceived("", sensorMessage); + sensorThingHandler.messageReceived(sensorMessage); // ASSERT - Mockito.verify(thingHandlerCallback).stateUpdated(eq(channelUID), eq(new DecimalType(129))); + Mockito.verify(thingHandlerCallback).stateUpdated(eq(channelUID), eq(new QuantityType<>("129 ppb"))); } @Test @@ -144,15 +145,23 @@ public class SensorsTest { SensorThermostatThingHandler sensorThingHandler = new SensorThermostatThingHandler(sensor, gson); sensorThingHandler.setCallback(thingHandlerCallback); - sensorThingHandler.messageReceived("", sensorMessage); - Mockito.verify(thingHandlerCallback).stateUpdated(eq(channelValveUID), - eq(new QuantityType<>(100.0, Units.PERCENT))); + sensorMessage = DeconzTest.getObjectFromJson("thermostat-undef.json", SensorMessage.class, gson); + assertNotNull(sensorMessage); + sensorThingHandler.messageReceived(sensorMessage); + + Mockito.verify(thingHandlerCallback).stateUpdated(eq(channelValveUID), eq(UnDefType.UNDEF)); Mockito.verify(thingHandlerCallback).stateUpdated(eq(channelHeatSetPointUID), eq(new QuantityType<>(25, SIUnits.CELSIUS))); Mockito.verify(thingHandlerCallback).stateUpdated(eq(channelModeUID), eq(new StringType(ThermostatMode.AUTO.name()))); Mockito.verify(thingHandlerCallback).stateUpdated(eq(channelTemperatureUID), eq(new QuantityType<>(16.5, SIUnits.CELSIUS))); + + sensorMessage = DeconzTest.getObjectFromJson("thermostat.json", SensorMessage.class, gson); + assertNotNull(sensorMessage); + sensorThingHandler.messageReceived(sensorMessage); + Mockito.verify(thingHandlerCallback).stateUpdated(eq(channelValveUID), + eq(new QuantityType<>(99, Units.PERCENT))); } @Test @@ -174,7 +183,7 @@ public class SensorsTest { SensorThingHandler sensorThingHandler = new SensorThingHandler(sensor, gson); sensorThingHandler.setCallback(thingHandlerCallback); - sensorThingHandler.messageReceived("", sensorMessage); + sensorThingHandler.messageReceived(sensorMessage); Mockito.verify(thingHandlerCallback).stateUpdated(eq(channelFireUID), eq(OnOffType.OFF)); Mockito.verify(thingHandlerCallback).stateUpdated(eq(channelBatteryLevelUID), eq(new DecimalType(98))); diff --git a/bundles/org.openhab.binding.deconz/src/test/resources/org/openhab/binding/deconz/fire.json b/bundles/org.openhab.binding.deconz/src/test/resources/org/openhab/binding/deconz/fire.json index 81ce08b18..2a7080454 100644 --- a/bundles/org.openhab.binding.deconz/src/test/resources/org/openhab/binding/deconz/fire.json +++ b/bundles/org.openhab.binding.deconz/src/test/resources/org/openhab/binding/deconz/fire.json @@ -3,7 +3,7 @@ "battery": 98, "on": true, "pending" : [], - "reachable": false + "reachable": true }, "ep": 1, "etag": "717549a99371f3ea1a5f0b40f1537094", diff --git a/bundles/org.openhab.binding.deconz/src/test/resources/org/openhab/binding/deconz/group.json b/bundles/org.openhab.binding.deconz/src/test/resources/org/openhab/binding/deconz/group.json new file mode 100644 index 000000000..0d3934d1e --- /dev/null +++ b/bundles/org.openhab.binding.deconz/src/test/resources/org/openhab/binding/deconz/group.json @@ -0,0 +1,30 @@ +{ + "action": { + "alert": "none", + "bri": 127, + "colormode": "hs", + "ct": 0, + "effect": "none", + "hue": 0, + "on": false, + "sat": 127, + "scene": null, + "xy": [ + 0, + 0 + ] + }, + "devicemembership": [ + "3" + ], + "etag": "586d2448a818aa7f6f3baa4907f43468", + "id": "1", + "lights": [], + "name": "RM01", + "scenes": [], + "state": { + "all_on": false, + "any_on": false + }, + "type": "LightGroup" +} \ No newline at end of file diff --git a/bundles/org.openhab.binding.deconz/src/test/resources/org/openhab/binding/deconz/thermostat-undef.json b/bundles/org.openhab.binding.deconz/src/test/resources/org/openhab/binding/deconz/thermostat-undef.json new file mode 100644 index 000000000..0d314fe6d --- /dev/null +++ b/bundles/org.openhab.binding.deconz/src/test/resources/org/openhab/binding/deconz/thermostat-undef.json @@ -0,0 +1,27 @@ +{ + "config": { + "battery": 85, + "displayflipped": null, + "heatsetpoint": 2500, + "locked": null, + "mode": "auto", + "offset": 0, + "on": true, + "reachable": true + }, + "ep": 1, + "etag": "717549a99371f3ea1a5f0b40f1537094", + "lastseen": "2020-05-31T20:24:55.819", + "manufacturername": "Eurotronic", + "modelid": "SPZB0001", + "name": "Test Thermostat", + "state": { + "lastupdated": "2020-05-31T20:24:55.819", + "on": true, + "temperature": 1650, + "valve": 255 + }, + "swversion": "20191014", + "type": "ZHAThermostat", + "uniqueid": "00:15:8d:00:01:ff:8a:00-01-0201" +} \ No newline at end of file diff --git a/bundles/org.openhab.binding.deconz/src/test/resources/org/openhab/binding/deconz/thermostat.json b/bundles/org.openhab.binding.deconz/src/test/resources/org/openhab/binding/deconz/thermostat.json index 0d314fe6d..e92f57bbe 100644 --- a/bundles/org.openhab.binding.deconz/src/test/resources/org/openhab/binding/deconz/thermostat.json +++ b/bundles/org.openhab.binding.deconz/src/test/resources/org/openhab/binding/deconz/thermostat.json @@ -19,7 +19,7 @@ "lastupdated": "2020-05-31T20:24:55.819", "on": true, "temperature": 1650, - "valve": 255 + "valve": 99 }, "swversion": "20191014", "type": "ZHAThermostat",