]> git.basschouten.com Git - openhab-addons.git/commitdiff
[deconz] Add Pairing/Scene actions, new devices and improve code (#14622)
authorJ-N-K <github@klug.nrw>
Sat, 18 Mar 2023 15:06:55 +0000 (16:06 +0100)
committerGitHub <noreply@github.com>
Sat, 18 Mar 2023 15:06:55 +0000 (16:06 +0100)
* port changes
* update instructions
* Incorporate review comments from #14134
* new improvements (mostly Java 17 changes)
* further improvements

Signed-off-by: Jan N. Klug <github@klug.nrw>
48 files changed:
CODEOWNERS
bundles/org.openhab.binding.deconz/README.md
bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/BindingConstants.java
bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/DeconzDynamicCommandDescriptionProvider.java
bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/DeconzDynamicStateDescriptionProvider.java
bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/Util.java
bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/action/BridgeActions.java [new file with mode: 0644]
bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/action/GroupActions.java [new file with mode: 0644]
bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/discovery/BridgeDiscoveryParticipant.java
bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/discovery/ThingDiscoveryService.java
bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/dto/DeconzBaseMessage.java
bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/dto/GroupAction.java
bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/dto/GroupState.java
bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/dto/LightState.java
bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/dto/NewSceneResponse.java [new file with mode: 0644]
bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/dto/SensorConfig.java
bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/dto/SensorState.java
bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/dto/ThermostatUpdateConfig.java
bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/handler/DeconzBaseThingHandler.java
bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/handler/DeconzBridgeConfig.java
bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/handler/DeconzBridgeHandler.java
bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/handler/GroupThingHandler.java
bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/handler/LightThingHandler.java
bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/handler/SensorBaseThingHandler.java
bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/handler/SensorThermostatThingHandler.java
bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/handler/SensorThingHandler.java
bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/netutils/AsyncHttpClient.java
bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/netutils/WebSocketConnection.java
bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/netutils/WebSocketConnectionListener.java
bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/netutils/WebSocketMessageListener.java
bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/types/GroupType.java
bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/types/LightType.java
bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/types/ResourceType.java
bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/types/ThermostatMode.java
bundles/org.openhab.binding.deconz/src/main/resources/OH-INF/config/config.xml
bundles/org.openhab.binding.deconz/src/main/resources/OH-INF/i18n/deconz.properties
bundles/org.openhab.binding.deconz/src/main/resources/OH-INF/thing/group-thing-types.xml
bundles/org.openhab.binding.deconz/src/main/resources/OH-INF/thing/light-thing-types.xml
bundles/org.openhab.binding.deconz/src/main/resources/OH-INF/thing/sensor-thing-types.xml
bundles/org.openhab.binding.deconz/src/main/resources/OH-INF/update/update.xml [new file with mode: 0644]
bundles/org.openhab.binding.deconz/src/test/java/org/openhab/binding/deconz/DeconzTest.java
bundles/org.openhab.binding.deconz/src/test/java/org/openhab/binding/deconz/LightGroupTest.java [new file with mode: 0644]
bundles/org.openhab.binding.deconz/src/test/java/org/openhab/binding/deconz/LightsTest.java
bundles/org.openhab.binding.deconz/src/test/java/org/openhab/binding/deconz/SensorsTest.java
bundles/org.openhab.binding.deconz/src/test/resources/org/openhab/binding/deconz/fire.json
bundles/org.openhab.binding.deconz/src/test/resources/org/openhab/binding/deconz/group.json [new file with mode: 0644]
bundles/org.openhab.binding.deconz/src/test/resources/org/openhab/binding/deconz/thermostat-undef.json [new file with mode: 0644]
bundles/org.openhab.binding.deconz/src/test/resources/org/openhab/binding/deconz/thermostat.json

index d10b6a3c1c52af68b7d79a0a2b35240a566718b7..5be07e4077702eaf5523a3bc1a5ab4acda5fba43 100644 (file)
@@ -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
index 13723a4571b85d347ed525b9b429812c4bd7513c..be8def99a340be4f1ca000ece58fd846d218a0d9 100644 (file)
@@ -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`       |
-
-Additionally lights, window coverings (blinds), door locks and thermostats 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                 | `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:
 
 | 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                                        |
-
-**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.
+| 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.
 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.
index a7b81419a3dcae96503b21dd68947b51bf9fab5a..1d8c6c58aca3a51eb1cc825177ef2a0f8608a6b4 100644 (file)
@@ -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";
index 4bb7198ada01d7f4ffa39fb1ff34e4feb449fcf4..57a23dd0420e557aba949237e27e15ec812f7fbe 100644 (file)
 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));
     }
index 77b892d59cc1f5997c89bec98d5f734da734af2f..4a388e51ed073c32730b5953b2135cfb2c9e0fa4 100644 (file)
@@ -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<ChannelUID, StateDescriptionFragment> 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));
         }
     }
index f41287b1934ee076e1b98fe5bdfb71717fc5d2c3..584d0140994cf7505a7761983162aefa3522502c 100644 (file)
@@ -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<K> getKeysFromValue(Map<K, V> 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 (file)
index 0000000..ec8a1fd
--- /dev/null
@@ -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 (file)
index 0000000..79f0057
--- /dev/null
@@ -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<List<NewSceneResponse>>() {
+    }.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<String, Object> 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<String> newSceneId = new CompletableFuture<>();
+        handler.doNetwork(Map.of("name", name), "scenes", HttpMethod.POST, newSceneId::complete);
+
+        try {
+            String returnedJson = newSceneId.get(2000, TimeUnit.MILLISECONDS);
+            List<NewSceneResponse> 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<String, Object> 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;
+    }
+}
index 929469360ce22b9d565ab7f67e5713d3fcbdda36..74894aafa695b84fe901d07801fb07a30d4a6bf0 100644 (file)
@@ -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();
index fafcbbbbf643b643f00263a2778bdda7fd811e75..0c391c44f77d4da94b2650890ebbf1ea6750ea59 100644 (file)
@@ -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();
     }
 }
index bdd2019e72802ca254a203f42e4f1a61c5d5cfda..503a78614dc2d64f45c96aaa26cbcbb03de4eec5 100644 (file)
@@ -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 = "";
index 2abb183633ff538cb9fca3367700cde15f95e0d0..9968d2c593c5f4ca03374bd666a24d6bae1c8477 100644 (file)
@@ -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
index fae765d845dafcde3dddad3c480245fc0af52b39..2cbcc4035c8c439b7d2f12005d18343cbc726305 100644 (file)
@@ -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 + '}';
     }
 }
index 88e55ad84eab9e2b5a27364801cc4d0afa6ef164..8622acfc9e25c839c14c71da4d5004fff1a28691 100644 (file)
@@ -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 (file)
index 0000000..3215988
--- /dev/null
@@ -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;
+    }
+}
index d46ae5b3bc65f9772b1db64b876e191b6c1b129c..84355cf0ced783ac945210d21e0a3e5593b12517 100644 (file)
@@ -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 + "}";
     }
 }
index 69634d32921913a7da25e9e74785953d1a4368ea..2d38f8c0bb142dcc1a649ae6959e5639a82437ed 100644 (file)
@@ -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) + "}";
     }
 }
index cbce35122f83a5c3884127a88bbd4397f0175895..4b7a36663b3071bdf376ea7ced25c1c6599a0146 100644 (file)
@@ -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;
 }
index 8717620c745ddabb9191cdf2209b3e17d0e964da..7ae7a6cfa8265a41cfc31f040eb602fcb82d1a71 100644 (file)
 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<DeconzBaseMessage> 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<String> 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 <code>null</code> or {@param on} is <code>false</code> 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);
         }
     }
 }
index bb8394812dfc5167d419dcc99dd4851aa55779fa..dd375bd1d4b8f06916195b0cd63c8fc01856732d 100644 (file)
@@ -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;
index e5369a30ff8a1eddd65d93c69d6e4c97fa0399f0..a81b107f47c8e6bfca0b735b492dc3169c7b61c6 100644 (file)
@@ -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<ThingTypeUID> SUPPORTED_THING_TYPES = Collections.singleton(BRIDGE_TYPE);
+    public static final Set<ThingTypeUID> 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<Optional<BridgeFullState>> 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<Class<? extends ThingHandlerService>> 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.<BridgeFullState> 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<AsyncHttpClient.Result> sendObject(String endPoint, @Nullable Object object) {
+    public CompletableFuture<AsyncHttpClient.Result> 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);
+
+        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 http.put(url, json, config.timeout);
+        return CompletableFuture.failedFuture(new IllegalArgumentException("Unknown HTTP Method"));
     }
 }
index 02db57957c99b9fff081e134143aeca72127ed2e..04a8144d56eb22716677b5a94821861a53a4d59d 100644 (file)
 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<String> getIdFromSceneName(String sceneName) {
+        CompletableFuture<String> 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<State> getSceneNameFromId(String sceneId) {
+        CompletableFuture<State> 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<String, String> processScenes(DeconzBaseMessage stateResponse) {
+        if (stateResponse instanceof GroupMessage groupMessage) {
+            Map<String, String> 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<Class<? extends ThingHandlerService>> getServices() {
+        return Set.of(GroupActions.class);
+    }
 }
index 70a9bf574736290a008c94353fb3ceed4b654a0e..c6e903dc4797fc6d88551ce1f737af6cd9420ead 100644 (file)
@@ -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;
+        }
+
+        if (checkLastSeen(thingBuilder, stateResponse.lastseen)) {
+            thingEdited = true;
+        }
+        if (thingEdited) {
+            updateThing(thingBuilder.build());
         }
 
-        messageReceived(config.id, lightMessage);
+        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<String> 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<CommandOption> toCommandOptionList(List<String> 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)));
-                }
-                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));
+                    updateState(channelUID, new DecimalType(miredToKelvin(ct)));
                 }
-                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)));
+        }
+    }
 }
index 55aceb248ef73d6172d94a3dd5f8cb379f48b697..004d769d098dc3b8c445d809c07ab35b6ec0093d 100644 (file)
@@ -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<String> 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,33 +113,37 @@ 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);
-
-        ignoreConfigurationUpdate = false;
+        if (checkLastSeen(thingBuilder, sensorMessage.lastseen)) {
+            thingEdited = true;
+        }
 
-        // "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);
+        // if the thing was edited, we update it now
+        if (thingEdited) {
+            logger.debug("Thing configuration changed, updating thing.");
+            updateThing(thingBuilder.build());
         }
+        ignoreConfigurationUpdate = false;
 
         // Initial data
         updateChannels(sensorConfig);
@@ -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<String> 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));
-    }
 }
index 9fe4cbd30af3c2ac4110967b45122f454db93d1a..0f04d09e06d815afb4b3a4fef7ac36db762d3846 100644 (file)
@@ -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<ThingTypeUID> SUPPORTED_THING_TYPES = Collections.singleton(THING_TYPE_THERMOSTAT);
 
     private static final List<String> 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);
index ca1538e1e8fb46d109cbc85ceecdd388b80f3c6e..9dc3eb4045c496dbfd7a7e6e35c4f074e77244f2 100644 (file)
@@ -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<String> 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
index 5ca7c68e6afbe0ae85276f107c1daa4ead9535e1..e5c2fa6fd6ac00eb8ff4741cc85365544c41c21b 100644 (file)
@@ -48,7 +48,7 @@ public class AsyncHttpClient {
      * @param timeout A timeout
      * @return The result
      */
-    public CompletableFuture<Result> post(String address, String jsonString, int timeout) {
+    public CompletableFuture<Result> 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;
index f2c3d4e4cd5e505f11f5ac0934d5a4605aa45bce..9b31fa2d31e58dd68563d9fb9f9d2b81a0c0896a 100644 (file)
@@ -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<String, WebSocketMessageListener> 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);
+        }
+    }
+
+    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;
         }
     }
 
-    public void close() {
+    /**
+     * 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<? extends DeconzBaseMessage> 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
index a17c0847007d85f708b09273152839be6e5d10e0..44bfbfedfd4992060976c6f7e3b5f786f47189cc 100644 (file)
@@ -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);
 }
index 787a46ba882a74ed0f98d441813298c79dde0357..2befa091ffa99e1241b522502d2723268100820f 100644 (file)
@@ -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);
 }
index a8a4962ece667e2cd1465d355e5b88656b957a10..e383128230c7086909fca75c5a6a1ee5cffe5829 100644 (file)
@@ -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<String, GroupType> MAPPING = Arrays.stream(GroupType.values())
             .collect(Collectors.toMap(v -> v.type, v -> v));
     private static final Logger LOGGER = LoggerFactory.getLogger(GroupType.class);
 
-    private String type;
+    private final String type;
 
     GroupType(String type) {
         this.type = type;
index fb2be5f5636ed8cb8dee40b941a956a440b8c285..d611b2fabcfaf1c73053bf891d33148e7a5f724a 100644 (file)
@@ -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;
index 6c159a7e74b18a4a70e379e476cf2ca67565fd85..7059c0b26bba1d4dc666397b766db78d27b99da4 100644 (file)
@@ -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<String, ResourceType> MAPPING = Arrays.stream(ResourceType.values())
             .collect(Collectors.toMap(v -> v.identifier, v -> v));
     private static final Logger LOGGER = LoggerFactory.getLogger(ResourceType.class);
 
-    private String identifier;
-    private String commandUrl;
+    private final String identifier;
+    private final String commandUrl;
     private @Nullable Class<? extends DeconzBaseMessage> expectedMessageType;
 
     ResourceType(String identifier, String commandUrl,
index de3cf74ac44d629d4e087ba817946f0537a01afb..d38596654b8e1ff1890ab8394e295d6faa4b34a7 100644 (file)
@@ -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;
index bd0b4fab2fc77b7a22d73c57b3915e3be6709dc0..c9ac5f7fcb59fca153da2d360ced3ea4ad8dd379 100644 (file)
@@ -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">
 
        <config-description uri="thing-type:deconz:bridge">
+               <parameter-group name="http">
+                       <label>HTTP Connection</label>
+                       <advanced>true</advanced>
+               </parameter-group>
+               <parameter-group name="websocket">
+                       <label>Websocket Connection</label>
+                       <advanced>true</advanced>
+               </parameter-group>
                <parameter name="host" type="text" required="true">
                        <label>Host Address</label>
                        <context>network-address</context>
                        <description>IP address or host name of deCONZ interface.</description>
                </parameter>
-               <parameter name="httpPort" type="integer" min="1" max="65535">
-                       <label>HTTP Port</label>
-                       <description>Port of the deCONZ HTTP interface.</description>
-                       <default>80</default>
-               </parameter>
-               <parameter name="port" type="integer" min="1" max="65535">
-                       <label>Websocket Port</label>
-                       <description>Port of the deCONZ Websocket.</description>
-                       <advanced>true</advanced>
-               </parameter>
                <parameter name="apikey" type="text">
                        <label>API Key</label>
                        <context>password</context>
                        <description>If no API Key is provided, a new one will be requested. You need to authorize the access on the deCONZ
                                web interface.</description>
                </parameter>
-               <parameter name="timeout" type="integer" unit="ms" min="0">
+               <parameter name="httpPort" type="integer" min="1" max="65535" groupName="http">
+                       <label>Port</label>
+                       <description>Port of the deCONZ HTTP interface.</description>
+                       <advanced>true</advanced>
+                       <default>80</default>
+               </parameter>
+               <parameter name="timeout" type="integer" unit="ms" min="0" groupName="http">
                        <label>Timeout</label>
                        <description>Timeout for asynchronous HTTP requests (in milliseconds).</description>
                        <advanced>true</advanced>
                        <default>2000</default>
                </parameter>
+               <parameter name="port" type="integer" min="1" max="65535" groupName="websocket">
+                       <label>Port</label>
+                       <description>Port of the deCONZ Websocket.</description>
+                       <advanced>true</advanced>
+               </parameter>
+               <parameter name="websocketTimeout" type="integer" unit="s" min="30" groupName="websocket">
+                       <label>Timeout</label>
+                       <description>Timeout for the websocket connection (in seconds).</description>
+                       <advanced>true</advanced>
+                       <default>120</default>
+               </parameter>
+       </config-description>
+
+       <config-description uri="thing-type:deconz:lightgroup">
+               <parameter name="id" type="text" required="true">
+                       <label>Device ID</label>
+                       <description>The deCONZ bridge assigns an integer number ID to each group.</description>
+               </parameter>
+               <parameter name="transitiontime" type="decimal" min="0" unit="s">
+                       <label>Transition Time</label>
+                       <description>Time to move between two states. If empty, the default of the group is used. Resolution is 1/10 second.</description>
+               </parameter>
+               <parameter name="colormode" type="text">
+                       <label>Color Mode</label>
+                       <description>Override the default color mode (auto-detect)</description>
+                       <options>
+                               <option value="hs">HSB</option>
+                               <option value="xy">XY</option>
+                       </options>
+                       <advanced>true</advanced>
+               </parameter>
        </config-description>
 
        <config-description uri="thing-type:deconz:sensor">
                        <label>Transition Time</label>
                        <description>Time to move between two states. If empty, the default of the device is used. Resolution is 1/10 second.</description>
                </parameter>
+               <parameter name="lastSeenPolling" type="integer" min="0" unit="min">
+                       <label>LastSeen Poll Interval</label>
+                       <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).</description>
+                       <default>1440</default>
+               </parameter>
        </config-description>
 
        <config-description uri="thing-type:deconz:colorlight">
                        </options>
                        <advanced>true</advanced>
                </parameter>
-       </config-description>
-
-       <config-description uri="thing-type:deconz:lightgroup">
-               <parameter name="id" type="text" required="true">
-                       <label>Device ID</label>
-                       <description>The deCONZ bridge assigns an integer number ID to each group.</description>
-               </parameter>
-               <parameter name="colormode" type="text">
-                       <label>Color Mode</label>
-                       <description>Override the default color mode (auto-detect)</description>
-                       <options>
-                               <option value="hs">HSB</option>
-                               <option value="xy">XY</option>
-                       </options>
-                       <advanced>true</advanced>
+               <parameter name="lastSeenPolling" type="integer" min="0" unit="min">
+                       <label>LastSeen Poll Interval</label>
+                       <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).</description>
+                       <default>1440</default>
                </parameter>
        </config-description>
+
 </config-description:config-descriptions>
index c2e8345ec2258b0aafeb8158f359f0e38f698ef6..5f507c1bbde1e8797c07655915bd475e5ac97729 100644 (file)
@@ -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.
index 80be02746e4546002e4f2ad7a1eaa8079662fbb2..df389ed9eb0d034c4c10cd38e7e3686e58579c33 100644 (file)
                        <channel typeId="scene" id="scene"/>
                </channels>
 
+               <properties>
+                       <property name="thingTypeVersion">1</property>
+               </properties>
+
                <representation-property>uid</representation-property>
 
                <config-description-ref uri="thing-type:deconz:lightgroup"/>
@@ -28,6 +32,9 @@
                <item-type>Switch</item-type>
                <label>All On</label>
                <description>"On" if all lights in this group are "On", otherwise "Off".</description>
+               <tags>
+                       <tag>Lighting</tag>
+               </tags>
                <state readOnly="true"/>
        </channel-type>
 
                <item-type>Switch</item-type>
                <label>Any On</label>
                <description>"On" if any light in this group is "On", otherwise "Off".</description>
+               <tags>
+                       <tag>Lighting</tag>
+               </tags>
                <state readOnly="true"/>
        </channel-type>
 
        <channel-type id="scene">
                <item-type>String</item-type>
                <label>Recall Scene</label>
+               <tags>
+                       <tag>Lighting</tag>
+               </tags>
        </channel-type>
 
 </thing:thing-descriptions>
index fa134f4a7b3c763320b35bc6b66fdd94c41c2b35..7130c6148749f586156653b4a3145a2aa51665ec 100644 (file)
@@ -9,10 +9,9 @@
                        <bridge-type-ref id="deconz"/>
                </supported-bridge-type-refs>
                <label>Warning Device</label>
-               <description>A warning device</description>
                <category>Siren</category>
                <channels>
-                       <channel id="alert" typeId="alert"></channel>
+                       <channel typeId="alert" id="alert"/>
                </channels>
 
                <representation-property>uid</representation-property>
                <supported-bridge-type-refs>
                        <bridge-type-ref id="deconz"/>
                </supported-bridge-type-refs>
+
                <label>On/Off Light</label>
                <description>A light that can be turned on or off.</description>
+
                <channels>
                        <channel typeId="system.power" id="switch"/>
                        <channel typeId="ontime" id="ontime"/>
                </channels>
 
+               <properties>
+                       <property name="thingTypeVersion">1</property>
+               </properties>
+
                <representation-property>uid</representation-property>
 
                <config-description-ref uri="thing-type:deconz:sensor"/>
                        <bridge-type-ref id="deconz"/>
                </supported-bridge-type-refs>
                <label>Dimmable Light</label>
-               <description>A dimmable light.</description>
                <category>Lightbulb</category>
                <channels>
                        <channel typeId="system.brightness" id="brightness"/>
                        <channel typeId="ontime" id="ontime"/>
-                       <channel id="alert" typeId="alert"></channel>
+                       <channel typeId="alert" id="alert"/>
                </channels>
 
+               <properties>
+                       <property name="thingTypeVersion">1</property>
+               </properties>
+
                <representation-property>uid</representation-property>
 
                <config-description-ref uri="thing-type:deconz:light"/>
                        <channel typeId="system.brightness" id="brightness"/>
                        <channel typeId="ct" id="color_temperature"/>
                        <channel typeId="ontime" id="ontime"/>
-                       <channel id="alert" typeId="alert"></channel>
+                       <channel typeId="alert" id="alert"/>
                </channels>
 
+               <properties>
+                       <property name="thingTypeVersion">1</property>
+               </properties>
+
                <representation-property>uid</representation-property>
 
                <config-description-ref uri="thing-type:deconz:light"/>
                <channels>
                        <channel typeId="system.color" id="color"/>
                        <channel typeId="ontime" id="ontime"/>
-                       <channel id="alert" typeId="alert"></channel>
+                       <channel typeId="alert" id="alert"/>
                </channels>
 
+               <properties>
+                       <property name="thingTypeVersion">1</property>
+               </properties>
+
                <representation-property>uid</representation-property>
 
                <config-description-ref uri="thing-type:deconz:colorlight"/>
                        <channel typeId="system.color" id="color"/>
                        <channel typeId="ct" id="color_temperature"/>
                        <channel typeId="ontime" id="ontime"/>
-                       <channel id="alert" typeId="alert"></channel>
+                       <channel typeId="alert" id="alert"/>
                </channels>
 
+               <properties>
+                       <property name="thingTypeVersion">1</property>
+               </properties>
+
                <representation-property>uid</representation-property>
 
                <config-description-ref uri="thing-type:deconz:colorlight"/>
                <item-type>Number:Time</item-type>
                <label>On Time</label>
                <description>Time that the light stays on before switched off automatically (0=forever)</description>
+               <state pattern="%.1f %unit%" min="0"/>
        </channel-type>
 
        <channel-type id="effect">
                <item-type>String</item-type>
                <label>Effect Channel</label>
+               <tags>
+                       <tag>Lighting</tag>
+               </tags>
        </channel-type>
 
        <channel-type id="effectSpeed">
                <item-type>Number</item-type>
                <label>Effect Speed Channel</label>
+               <tags>
+                       <tag>Lighting</tag>
+               </tags>
                <state min="0" max="10" step="1"/>
        </channel-type>
 
index 07b59216d4d420ab3c914c62392484e29739cf76..60ab111226470c44fe338ce5d549ce77851e61fc 100644 (file)
@@ -9,13 +9,16 @@
                        <bridge-type-ref id="deconz"/>
                </supported-bridge-type-refs>
                <label>Presence Sensor</label>
-               <description>A Presence sensor</description>
                <channels>
                        <channel typeId="system.motion" id="presence"/>
                        <channel typeId="last_updated" id="last_updated"/>
                        <channel typeId="system.power" id="enabled"/>
                </channels>
 
+               <properties>
+                       <property name="thingTypeVersion">1</property>
+               </properties>
+
                <representation-property>uid</representation-property>
 
                <config-description-ref uri="thing-type:deconz:sensor"/>
@@ -42,7 +45,6 @@
                        <bridge-type-ref id="deconz"/>
                </supported-bridge-type-refs>
                <label>Power Sensor</label>
-               <description>A power sensor</description>
                <channels>
                        <channel typeId="power" id="power"/>
                        <channel typeId="last_updated" id="last_updated"/>
@@ -58,7 +60,7 @@
                <label>Power</label>
                <description>Current power usage</description>
                <category>Energy</category>
-               <state readOnly="true" pattern="%.1f %unit%"></state>
+               <state readOnly="true" pattern="%.1f %unit%"/>
        </channel-type>
 
        <channel-type id="voltage">
                        <bridge-type-ref id="deconz"/>
                </supported-bridge-type-refs>
                <label>Consumption Sensor</label>
-               <description>A consumption sensor</description>
                <channels>
-                       <channel typeId="consumption" id="consumption"></channel>
-                       <channel typeId="last_updated" id="last_updated"></channel>
+                       <channel typeId="consumption" id="consumption"/>
+                       <channel typeId="last_updated" id="last_updated"/>
                </channels>
 
                <representation-property>uid</representation-property>
@@ -98,7 +99,7 @@
                <label>Consumption</label>
                <description>Current consumption</description>
                <category>Energy</category>
-               <state readOnly="true" pattern="%.1f %unit%"></state>
+               <state readOnly="true" pattern="%.1f %unit%"/>
        </channel-type>
 
        <thing-type id="colorcontrol">
                        <channel typeId="last_updated" id="last_updated"/>
                </channels>
 
+               <properties>
+                       <property name="thingTypeVersion">1</property>
+               </properties>
+
                <representation-property>uid</representation-property>
 
                <config-description-ref uri="thing-type:deconz:sensor"/>
                        <bridge-type-ref id="deconz"/>
                </supported-bridge-type-refs>
                <label>Switch/Button</label>
-               <description>A switch or button</description>
                <channels>
                        <channel typeId="buttonevent" id="buttonevent"/>
                        <channel typeId="button" id="button"/>
                <item-type>Number</item-type>
                <label>Button</label>
                <description>The Button that was last pressed on the switch.</description>
-               <state readOnly="true" pattern="%d"></state>
+               <state readOnly="true" pattern="%d"/>
        </channel-type>
 
        <channel-type id="gestureevent">
                        <bridge-type-ref id="deconz"/>
                </supported-bridge-type-refs>
                <label>Light Sensor</label>
-               <description>A light sensor</description>
                <channels>
                        <channel typeId="lightlux" id="lightlux"/>
                        <channel typeId="light_level" id="light_level"/>
                <item-type>Number:Illuminance</item-type>
                <label>Illuminance</label>
                <description>Current light illuminance</description>
-               <state readOnly="true" pattern="%.1f %unit%"></state>
+               <state readOnly="true" pattern="%.1f %unit%"/>
        </channel-type>
 
        <channel-type id="light_level" advanced="true">
                        <bridge-type-ref id="deconz"/>
                </supported-bridge-type-refs>
                <label>Temperature Sensor</label>
-               <description>A temperature sensor</description>
                <channels>
                        <channel typeId="temperature" id="temperature"/>
                        <channel typeId="last_updated" id="last_updated"/>
                <label>Temperature</label>
                <description>Current temperature</description>
                <category>Temperature</category>
-               <state readOnly="true" pattern="%.2f %unit%"></state>
+               <state readOnly="true" pattern="%.2f %unit%"/>
        </channel-type>
 
        <thing-type id="humiditysensor">
                        <bridge-type-ref id="deconz"/>
                </supported-bridge-type-refs>
                <label>Humidity Sensor</label>
-               <description>A humidity sensor</description>
                <channels>
                        <channel typeId="humidity" id="humidity"/>
                        <channel typeId="last_updated" id="last_updated"/>
                <label>Humidity</label>
                <description>Current humidity</description>
                <category>Humidity</category>
-               <state readOnly="true" pattern="%.2f %unit%"></state>
+               <state readOnly="true" pattern="%.2f %unit%"/>
        </channel-type>
 
        <thing-type id="pressuresensor">
                        <bridge-type-ref id="deconz"/>
                </supported-bridge-type-refs>
                <label>Pressure Sensor</label>
-               <description>A pressure senor</description>
                <channels>
-                       <channel typeId="pressure" id="pressure"></channel>
-                       <channel typeId="last_updated" id="last_updated"></channel>
+                       <channel typeId="pressure" id="pressure"/>
+                       <channel typeId="last_updated" id="last_updated"/>
                </channels>
 
                <representation-property>uid</representation-property>
                <label>Pressure</label>
                <description>Current pressure</description>
                <category>Pressure</category>
-               <state readOnly="true" pattern="%.1f %unit%"></state>
+               <state readOnly="true" pattern="%.1f %unit%"/>
        </channel-type>
 
        <thing-type id="daylightsensor">
                        <bridge-type-ref id="deconz"/>
                </supported-bridge-type-refs>
                <label>Daylight Sensor</label>
-               <description>A daylight sensor</description>
                <channels>
-                       <channel typeId="value" id="value"></channel>
-                       <channel typeId="light" id="light"></channel>
+                       <channel typeId="value" id="value"/>
+                       <channel typeId="light" id="light"/>
                </channels>
 
                <representation-property>uid</representation-property>
                <item-type>Number</item-type>
                <label>Daylight Value</label>
                <description>Dawn is around 130, sunrise at 140, sunset at 190, and dusk at 210</description>
-               <state readOnly="true"></state>
+               <state readOnly="true"/>
        </channel-type>
 
        <channel-type id="light">
                <item-type>String</item-type>
                <label>Lightlevel</label>
-               <description>A light level</description>
                <state readOnly="true">
                        <options>
                                <option value="daylight">Daylight</option>
                        <bridge-type-ref id="deconz"/>
                </supported-bridge-type-refs>
                <label>Open/Close Sensor</label>
-               <description>An open/close sensor</description>
                <channels>
                        <channel typeId="open" id="open"/>
                        <channel typeId="last_updated" id="last_updated"/>
                <item-type>Contact</item-type>
                <label>Open/Close</label>
                <description>Open/Close detected</description>
-               <state readOnly="true"></state>
+               <state readOnly="true"/>
        </channel-type>
 
        <thing-type id="waterleakagesensor">
                        <bridge-type-ref id="deconz"/>
                </supported-bridge-type-refs>
                <label>Water Leakage Sensor</label>
-               <description>A water leakage sensor</description>
                <channels>
                        <channel typeId="waterleakage" id="waterleakage"/>
                        <channel typeId="last_updated" id="last_updated"/>
                        <bridge-type-ref id="deconz"/>
                </supported-bridge-type-refs>
                <label>Fire Sensor</label>
-               <description>A fire sensor</description>
                <channels>
                        <channel typeId="fire" id="fire"/>
                        <channel typeId="last_updated" id="last_updated"/>
                        <bridge-type-ref id="deconz"/>
                </supported-bridge-type-refs>
                <label>Alarm Sensor</label>
-               <description>An alarm sensor</description>
                <channels>
                        <channel typeId="alarm" id="alarm"/>
                        <channel typeId="last_updated" id="last_updated"/>
                        <bridge-type-ref id="deconz"/>
                </supported-bridge-type-refs>
                <label>Vibration Sensor</label>
-               <description>A vibration sensor</description>
                <channels>
                        <channel typeId="vibration" id="vibration"/>
                        <channel typeId="last_updated" id="last_updated"/>
                        <bridge-type-ref id="deconz"/>
                </supported-bridge-type-refs>
                <label>Battery Sensor</label>
-               <description>A battery sensor</description>
                <channels>
                        <channel typeId="system.battery-level" id="battery_level"/>
                        <channel typeId="last_updated" id="last_updated"/>
                </channels>
 
+               <properties>
+                       <property name="thingTypeVersion">1</property>
+               </properties>
+
                <representation-property>uid</representation-property>
 
                <config-description-ref uri="thing-type:deconz:sensor"/>
                <state readOnly="true"/>
        </channel-type>
 
-
        <thing-type id="airqualitysensor">
                <supported-bridge-type-refs>
                        <bridge-type-ref id="deconz"/>
                </supported-bridge-type-refs>
-               <label>Air quality Sensor</label>
-               <description>An air quality sensor</description>
+               <label>Carbon-monoxide Sensor</label>
                <channels>
                        <channel typeId="airquality" id="airquality"/>
                        <channel typeId="airqualityppb" id="airqualityppb"/>
 
        <channel-type id="airquality">
                <item-type>String</item-type>
-               <label>Air quality level</label>
+               <label>Air Quality</label>
                <description>Current air quality level based on volatile organic compounds (VOCs) measurement. Example: good or poor,
                        ...</description>
-               <state readOnly="true" pattern="%s"></state>
+               <state readOnly="true"/>
        </channel-type>
 
        <channel-type id="airqualityppb">
                <item-type>Number:Dimensionless</item-type>
-               <label>Air quality in ppb</label>
+               <label>Air Quality (ppb)</label>
                <description>Current air quality based on measurements of volatile organic compounds (VOCs). The measured value is
                        specified in ppb (parts per billion).</description>
-               <state readOnly="true" pattern="%d"></state>
+               <state readOnly="true"/>
        </channel-type>
 
+       <thing-type id="moisturesensor">
+               <supported-bridge-type-refs>
+                       <bridge-type-ref id="deconz"/>
+               </supported-bridge-type-refs>
+               <label>Moisture Sensor</label>
+               <channels>
+                       <channel typeId="moisture" id="moisture"/>
+                       <channel typeId="last_updated" id="last_updated"/>
+               </channels>
+
+               <representation-property>uid</representation-property>
+
+               <config-description-ref uri="thing-type:deconz:sensor"/>
+       </thing-type>
+
+       <channel-type id="moisture">
+               <item-type>Number:Dimensionless</item-type>
+               <label>Moisture</label>
+               <description>Current moisture</description>
+               <state readOnly="true" pattern="%.1f %unit%"/>
+       </channel-type>
 
        <thing-type id="thermostat">
                <supported-bridge-type-refs>
                <config-description-ref uri="thing-type:deconz:sensor"/>
        </thing-type>
 
+       <channel-type id="locked">
+               <item-type>Switch</item-type>
+               <label>Locked</label>
+               <description>Status of this thermostat's child lock.</description>
+               <category>Lock</category>
+       </channel-type>
+       <channel-type id="windowopen">
+               <item-type>Contact</item-type>
+               <label>Window Open</label>
+       </channel-type>
+       <channel-type id="externalwindowopen">
+               <item-type>Contact</item-type>
+               <label>External Window Open</label>
+       </channel-type>
        <channel-type id="heatsetpoint">
                <item-type>Number:Temperature</item-type>
                <label>Target Temperature</label>
                <description>Target temperature</description>
                <category>Heating</category>
-               <state pattern="%.1f %unit%" step="0.5" max="28" min="6"></state>
+               <state pattern="%.1f %unit%" step="0.5" max="28" min="6"/>
        </channel-type>
        <channel-type id="mode">
                <item-type>String</item-type>
                <item-type>Number:Temperature</item-type>
                <label>Offset</label>
                <description>Temperature offset</description>
-               <state pattern="%.2f %unit%" step="0.01"></state>
+               <state pattern="%.2f %unit%" step="0.01"/>
        </channel-type>
        <channel-type id="valve">
                <item-type>Number:Dimensionless</item-type>
                <label>Valve position</label>
                <description>Current valve position</description>
-               <state readOnly="true" pattern="%.0f %unit%"/>
+               <state readOnly="true" pattern="%.1f %unit%"/>
        </channel-type>
 
 </thing:thing-descriptions>
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 (file)
index 0000000..e4639b0
--- /dev/null
@@ -0,0 +1,81 @@
+<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
+<update:update-descriptions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:update="https://openhab.org/schemas/update-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/update-description/v1.0.0 https://openhab.org/schemas/update-description-1.0.0.xsd">
+
+       <thing-type uid="deconz:batterysensor">
+               <instruction-set targetVersion="1">
+                       <update-channel id="battery_level">
+                               <type>system:battery-level</type>
+                       </update-channel>
+               </instruction-set>
+       </thing-type>
+
+       <thing-type uid="deconz:colorcontrol">
+               <instruction-set targetVersion="1">
+                       <update-channel id="color">
+                               <type>system:color</type>
+                       </update-channel>
+               </instruction-set>
+       </thing-type>
+
+       <thing-type uid="deconz:colorlight">
+               <instruction-set targetVersion="1">
+                       <update-channel id="color">
+                               <type>system:color</type>
+                       </update-channel>
+               </instruction-set>
+       </thing-type>
+
+       <thing-type uid="deconz:colortemperaturelight">
+               <instruction-set targetVersion="1">
+                       <update-channel id="brightness">
+                               <type>system:brightness</type>
+                       </update-channel>
+               </instruction-set>
+       </thing-type>
+
+       <thing-type uid="deconz:dimmablelight">
+               <instruction-set targetVersion="1">
+                       <update-channel id="brightness">
+                               <type>system:brightness</type>
+                       </update-channel>
+               </instruction-set>
+       </thing-type>
+
+       <thing-type uid="deconz:extendedcolorlight">
+               <instruction-set targetVersion="1">
+                       <update-channel id="color">
+                               <type>system:color</type>
+                       </update-channel>
+               </instruction-set>
+       </thing-type>
+
+       <thing-type uid="deconz:lightgroup">
+               <instruction-set targetVersion="1">
+                       <update-channel id="color">
+                               <type>system:color</type>
+                       </update-channel>
+               </instruction-set>
+       </thing-type>
+
+       <thing-type uid="deconz:onofflight">
+               <instruction-set targetVersion="1">
+                       <update-channel id="switch">
+                               <type>system:power</type>
+                       </update-channel>
+               </instruction-set>
+       </thing-type>
+
+       <thing-type uid="deconz:presencesensor">
+               <instruction-set targetVersion="1">
+                       <update-channel id="enabled">
+                               <type>system:power</type>
+                       </update-channel>
+                       <update-channel id="presence">
+                               <type>system:motion</type>
+                       </update-channel>
+               </instruction-set>
+       </thing-type>
+
+</update:update-descriptions>
index 8a0a512d4b54cd27dd71584ffc160dd96b505760..5091411cc93278d49a2a1667563e38f4ae0aeb78 100644 (file)
@@ -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 (file)
index 0000000..2a6ca1e
--- /dev/null
@@ -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));
+    }
+}
index e8a543187117bcfd5686197d661f33303524ac9e..f0e80d6b256ddfddc8071683251d5bd616457643 100644 (file)
@@ -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<String, String> 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")));
     }
 }
index daefaab40ece3ffcbaa8e5a1a97487d29fd39b33..1c9d51635c5117b4bb47d9d924e79d7cba5338bc 100644 (file)
@@ -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)));
index 81ce08b18be0c83bd2753f29bcbd4bc47232e789..2a7080454bca76781ce39676e8cbbb9289ab1e78 100644 (file)
@@ -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 (file)
index 0000000..0d3934d
--- /dev/null
@@ -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 (file)
index 0000000..0d314fe
--- /dev/null
@@ -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
index 0d314fe6d7ef44ffc45927ae9ea6d0220627bcb5..e92f57bbe90eeb07f00f8cac9182413a08ac9fec 100644 (file)
@@ -19,7 +19,7 @@
         "lastupdated": "2020-05-31T20:24:55.819",
         "on": true,
         "temperature": 1650,
-        "valve": 255
+        "valve": 99
     },
     "swversion": "20191014",
     "type": "ZHAThermostat",