[deconz] Add Pairing/Scene actions, new devices and improve code (#14622)

* port changes
* update instructions
* Incorporate review comments from #14134
* new improvements (mostly Java 17 changes)
* further improvements

Signed-off-by: Jan N. Klug <github@klug.nrw>
This commit is contained in:
J-N-K 2023-03-18 16:06:55 +01:00 committed by GitHub
parent 23f3374ea9
commit ee1de11864
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
48 changed files with 1814 additions and 824 deletions

View File

@ -64,7 +64,7 @@
/bundles/org.openhab.binding.dali/ @rs22 /bundles/org.openhab.binding.dali/ @rs22
/bundles/org.openhab.binding.danfossairunit/ @pravussum /bundles/org.openhab.binding.danfossairunit/ @pravussum
/bundles/org.openhab.binding.dbquery/ @lujop /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.denonmarantz/ @jwveldhuis
/bundles/org.openhab.binding.deutschebahn/ @soenkekueper /bundles/org.openhab.binding.deutschebahn/ @soenkekueper
/bundles/org.openhab.binding.digiplex/ @rmichalak /bundles/org.openhab.binding.digiplex/ @rmichalak

View 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. There is one bridge (`deconz`) that manages the connection to the deCONZ software instance.
These sensors are supported: These sensors are supported:
| Device type | Resource Type | Thing type | | Device type | Resource Type | Thing type |
|-----------------------------------|-----------------------------------|----------------------| |-----------------------------------|-----------------------------------|------------------------|
| Presence Sensor | ZHAPresence, CLIPPresence | `presencesensor` | | Presence Sensor | ZHAPresence, CLIPPresence | `presencesensor` |
| Power Sensor | ZHAPower, CLIPPower | `powersensor` | | Power Sensor | ZHAPower, CLIPPower | `powersensor` |
| Consumption Sensor | ZHAConsumption | `consumptionsensor` | | Consumption Sensor | ZHAConsumption | `consumptionsensor` |
| Switch | ZHASwitch | `switch` | | Switch | ZHASwitch | `switch` |
| Light Sensor | ZHALightLevel | `lightsensor` | | Light Sensor | ZHALightLevel | `lightsensor` |
| Temperature Sensor | ZHATemperature | `temperaturesensor` | | Temperature Sensor | ZHATemperature | `temperaturesensor` |
| Humidity Sensor | ZHAHumidity | `humiditysensor` | | Humidity Sensor | ZHAHumidity | `humiditysensor` |
| Pressure Sensor | ZHAPressure | `pressuresensor` | | Pressure Sensor | ZHAPressure | `pressuresensor` |
| Open/Close Sensor | ZHAOpenClose | `openclosesensor` | | Open/Close Sensor | ZHAOpenClose | `openclosesensor` |
| Water Leakage Sensor | ZHAWater | `waterleakagesensor` | | Water Leakage Sensor | ZHAWater | `waterleakagesensor` |
| Alarm Sensor | ZHAAlarm | `alarmsensor` | | Alarm Sensor | ZHAAlarm | `alarmsensor` |
| Fire Sensor | ZHAFire | `firesensor` | | Fire Sensor | ZHAFire | `firesensor` |
| Vibration Sensor | ZHAVibration | `vibrationsensor` | | Vibration Sensor | ZHAVibration | `vibrationsensor` |
| deCONZ Artificial Daylight Sensor | deCONZ specific: simulated sensor | `daylightsensor` | | deCONZ Artificial Daylight Sensor | deCONZ specific: simulated sensor | `daylightsensor` |
| Carbon-Monoxide Sensor | ZHACarbonmonoxide | `carbonmonoxide` | | Carbon-Monoxide Sensor | ZHACarbonmonoxide | `carbonmonoxidesensor` |
| Air quality Sensor | ZHAAirQuality | `airqualitysensor` | | Airquality Sensor | ZHAAirquality | `airqualitysensor` |
| Color Controller | ZBT-Remote-ALL-RGBW | `colorcontrol` | | Moisture Sensor | ZHAMoisture | `moisturesensor` |
| Color Controller | ZBT-Remote-ALL-RGBW | `colorcontrol` |
Additionally lights, window coverings (blinds), door locks and thermostats are supported: Additionally, lights, window coverings (blinds), door locks and thermostats are supported:
| Device type | Resource Type | Thing type | | 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` | | Warning Device (Siren) | Warning device | `warningdevice` |
| Door Lock | A remotely operatable door lock | `doorlock` | | 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`. Currently only light-groups are supported via the thing-type `lightgroup`.
## Discovery ## Discovery
@ -57,13 +60,14 @@ If your device is not discovered, please check the DEBUG log for unknown devices
These configuration parameters are available: These configuration parameters are available:
| Parameter | Description | Type | Default | | Parameter | Description | Type | Default |
|-----------|---------------------------------------------------------------------------------|---------|---------| |------------------|-------------------------------------------------------------------------------------------------------------------------|---------|---------|
| host | Host address (hostname / ip) of deCONZ interface | string | n/a | | host | Host address (hostname / ip) of deCONZ interface | string | n/a |
| httpPort | Port of deCONZ HTTP interface | string | 80 | | httpPort | Port of deCONZ HTTP interface | string | 80 |
| port | Port of deCONZ Websocket (optional, can be filled automatically) **(Advanced)** | string | n/a | | 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 | | apikey | Authorization API key (optional, can be filled automatically) | string | n/a |
| timeout | Timeout for asynchronous HTTP requests (in milliseconds) | integer | 2000 | | 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. 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. 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: The sensor devices support some of the following channels:
| Channel Type ID | Item Type | Access Mode | Description | Thing types | | Channel Type ID | Item Type | Access Mode | Description | Thing types |
|-----------------|--------------------------|-------------|-------------------------------------------------------------------------------------------|---------------------------------------------------| |--------------------|--------------------------|-------------|-------------------------------------------------------------------------------------------|---------------------------------------------------|
| presence | Switch | R | Status of presence: `ON` = presence; `OFF` = no-presence | presencesensor | | presence | Switch | R | Status of presence: `ON` = presence; `OFF` = no-presence | presencesensor |
| enabled | Switch | R/W | This channel activates or deactivates the sensor | 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_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 | | 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 | | power | Number:Power | R | Power usage in Watts | powersensor, sometimes for consumptionsensor |
| consumption | Number:Energy | R | Current power usage in Watts/Hour | consumptionsensor | | consumption | Number:Energy | R | Energy in Watt*Hour | consumptionsensor |
| voltage | Number:ElectricPotential | R | Current voltage in V | some powersensors | | voltage | Number:ElectricPotential | R | Voltage in V | some powersensors |
| current | Number:ElectricCurrent | R | Current current in mA | some powersensors | | current | Number:ElectricCurrent | R | Current in mA | some powersensors |
| button | Number | R | Last pressed button id on a switch | switch, colorcontrol | | button | Number | R | Last pressed button id on a switch | switch, colorcontrol |
| gesture | Number | R | A gesture that was performed with the switch | switch | | gesture | Number | R | A gesture that was performed with the switch | switch |
| lightlux | Number:Illuminance | R | Current light illuminance in Lux | lightsensor | | lightlux | Number:Illuminance | R | Light illuminance in Lux | lightsensor |
| light_level | Number | R | Current light level | lightsensor | | light_level | Number | R | Light level | lightsensor |
| dark | Switch | R | Light level is below the darkness threshold | lightsensor, sometimes for presencesensor | | 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 | | daylight | Switch | R | Light level is above the daylight threshold | lightsensor |
| temperature | Number:Temperature | R | Current temperature in ˚C | temperaturesensor, some Xiaomi sensors,thermostat | | temperature | Number:Temperature | R | Temperature in ˚C | temperaturesensor, some Xiaomi sensors,thermostat |
| humidity | Number:Dimensionless | R | Current humidity in % | humiditysensor | | humidity | Number:Dimensionless | R | Humidity in % | humiditysensor |
| pressure | Number:Pressure | R | Current pressure in hPa | pressuresensor | | pressure | Number:Pressure | R | Pressure in hPa | pressuresensor |
| open | Contact | R | Status of contacts: `OPEN`; `CLOSED` | openclosesensor | | 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 | | 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 | | 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 | | 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 | | 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 | | vibration | Switch | R | Status of vibration: `ON` = vibration was detected; `OFF` = no vibration | alarmsensor |
| light | String | R | Light level: `Daylight`; `Sunset`; `Dark` | daylightsensor | | light | String | R | Light level: `Daylight`; `Sunset`; `Dark` | daylightsensor |
| value | Number | R | Sun position: `130` = dawn; `140` = sunrise; `190` = sunset; `210` = dusk | 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_level | Number | R | Battery level (in %) | any battery-powered sensor |
| battery_low | Switch | R | Battery level low: `ON`; `OFF` | 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 | | carbonmonoxide | Switch | R | `ON` = carbon monoxide detected | carbonmonoxide |
| airquality | String | R | Current air quality level | airqualitysensor | | color | Color | R | Color set by remote | colorcontrol |
| airqualityppb | Number:Dimensionless | R | Current air quality ppb (parts per billion) | airqualitysensor | | windowopen | Contact | R | `windowopen` status is reported by some thermostats | thermostat |
| color | Color | R | Color set by remote | colorcontrol | | externalwindowopen | Contact | R/W | forward a status to a theromastat (some devices) | thermostat |
| windowopen | Contact | R | `windowopen` status is reported by some thermostats | thermostat | | locked | Switch | R/W | reports/sets the childlock on some thermostats | thermostat |
| airquality | String | R | Airquality as string | airqualitysensor |
| airqualityppb | Number:Dimensionless | R | Airquality (in parts-per-billion) | airqualitysensor |
| moisture | Number:Dimensionless | R | Moisture | moisturesensor |
**NOTE:** Beside other non mandatory channels, the `battery_level` and `battery_low` channels will be added to the Thing during runtime if the sensor is battery-powered. **NOTE:** Beside other non-mandatory channels, the `battery_level` and `battery_low` channels will be added to the Thing during runtime if the sensor is battery-powered.
The specification of your sensor depends on the deCONZ capabilities. 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). 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 Other devices support
| Channel Type ID | Item Type | Access Mode | Description | Thing types | | Channel Type ID | Item Type | Access Mode | Description | Thing types |
|-------------------|--------------------------|:-----------:|---------------------------------------|-------------------------------------------------| |-------------------|----------------------|:-----------:|---------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------|
| brightness | Dimmer | R/W | Brightness of the light | `dimmablelight`, `colortemperaturelight` | | brightness | Dimmer | R/W | Brightness of the light | `dimmablelight`, `colortemperaturelight` |
| switch | Switch | R/W | State of an ON/OFF device | `onofflight` | | switch | Switch | R/W | State of a ON/OFF device | `onofflight` |
| color | Color | R/W | Color of a multi-color light | `colorlight`, `extendedcolorlight`, `lightgroup`| | 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` | | 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` | | effect | String | R/W | Effect selection. Allowed commands are set dynamically | `colorlight` |
| effectSpeed | Number | W | Effect Speed | `colorlight` | | effectSpeed | Number | W | Effect Speed | `colorlight` |
| lock | Switch | R/W | Lock (ON) or unlock (OFF) the doorlock| `doorlock` | | 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 | | ontime | Number:Time | W | Timespan for which the light is turned on | all lights |
| position | Rollershutter | R/W | Position of the blind | `windowcovering` | | position | Rollershutter | R/W | Position of the blind | `windowcovering` |
| heatsetpoint | Number:Temperature | R/W | Target Temperature in °C | `thermostat` | | heatsetpoint | Number:Temperature | R/W | Target Temperature in °C | `thermostat` |
| valve | Number:Dimensionless | R | Valve position in % | `thermostat` | | valve | Number:Dimensionless | R | Valve position in % | `thermostat` |
| mode | String | R/W | Mode: "auto", "heat" and "off" | `thermostat` | | mode | String | R/W | Mode: "auto", "heat" and "off" | `thermostat` |
| offset | Number | R | Temperature offset for sensor | `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` | | 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` | | all_on | Switch | R | All lights in group are on | `lightgroup` |
| any_on | Switch | R | Any light in group is 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` | | 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. **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. 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_CLOCKWISE | 7 |
| GESTURE_ROTATE_COUNTER_CLOCKWISE | 8 | | 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 ## Full Example
### Things file ### Things file
@ -260,8 +287,34 @@ then
end 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 ### 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. 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. In that case the `transitiontime` parameter should be changed to the desired time.

View File

@ -15,6 +15,7 @@ package org.openhab.binding.deconz.internal;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.library.types.PercentType; import org.openhab.core.library.types.PercentType;
import org.openhab.core.thing.ThingTypeUID; import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.type.ChannelTypeUID;
/** /**
* The {@link BindingConstants} class defines common constants, which are * 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, public static final ThingTypeUID THING_TYPE_CARBONMONOXIDE_SENSOR = new ThingTypeUID(BINDING_ID,
"carbonmonoxidesensor"); "carbonmonoxidesensor");
public static final ThingTypeUID THING_TYPE_AIRQUALITY_SENSOR = new ThingTypeUID(BINDING_ID, "airqualitysensor"); 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 // Special sensor - Thermostat
public static final ThingTypeUID THING_TYPE_THERMOSTAT = new ThingTypeUID(BINDING_ID, "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_LAST_SEEN = "last_seen";
public static final String CHANNEL_POWER = "power"; public static final String CHANNEL_POWER = "power";
public static final String CHANNEL_CONSUMPTION = "consumption"; 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_VOLTAGE = "voltage";
public static final String CHANNEL_CURRENT = "current"; public static final String CHANNEL_CURRENT = "current";
public static final String CHANNEL_VALUE = "value"; 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_CARBONMONOXIDE = "carbonmonoxide";
public static final String CHANNEL_AIRQUALITY = "airquality"; public static final String CHANNEL_AIRQUALITY = "airquality";
public static final String CHANNEL_AIRQUALITYPPB = "airqualityppb"; 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_HEATSETPOINT = "heatsetpoint";
public static final String CHANNEL_THERMOSTAT_MODE = "mode"; 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_TEMPERATURE_OFFSET = "offset";
public static final String CHANNEL_VALVE_POSITION = "valve"; 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 // group + light channel ids
public static final String CHANNEL_SWITCH = "switch"; 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_SCENE = "scene";
public static final String CHANNEL_ONTIME = "ontime"; 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 // Thing configuration
public static final String CONFIG_HOST = "host"; public static final String CONFIG_HOST = "host";
public static final String CONFIG_HTTP_PORT = "httpPort"; public static final String CONFIG_HTTP_PORT = "httpPort";

View File

@ -13,10 +13,15 @@
package org.openhab.binding.deconz.internal; package org.openhab.binding.deconz.internal;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.events.EventPublisher;
import org.openhab.core.thing.ThingUID; import org.openhab.core.thing.ThingUID;
import org.openhab.core.thing.binding.BaseDynamicCommandDescriptionProvider; 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.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.Component;
import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -29,6 +34,16 @@ import org.slf4j.LoggerFactory;
@NonNullByDefault @NonNullByDefault
@Component(service = { DynamicCommandDescriptionProvider.class, DeconzDynamicCommandDescriptionProvider.class }) @Component(service = { DynamicCommandDescriptionProvider.class, DeconzDynamicCommandDescriptionProvider.class })
public class DeconzDynamicCommandDescriptionProvider extends BaseDynamicCommandDescriptionProvider { 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); private final Logger logger = LoggerFactory.getLogger(DeconzDynamicCommandDescriptionProvider.class);
/** /**
@ -36,7 +51,7 @@ public class DeconzDynamicCommandDescriptionProvider extends BaseDynamicCommandD
* *
* @param thingUID the thing's UID * @param thingUID the thing's UID
*/ */
public void removeDescriptionsForThing(ThingUID thingUID) { public void removeCommandDescriptionForThing(ThingUID thingUID) {
logger.trace("removing state description for thing {}", thingUID); logger.trace("removing state description for thing {}", thingUID);
channelOptionsMap.entrySet().removeIf(entry -> entry.getKey().getThingUID().equals(thingUID)); channelOptionsMap.entrySet().removeIf(entry -> entry.getKey().getThingUID().equals(thingUID));
} }

View File

@ -19,16 +19,20 @@ import java.util.concurrent.ConcurrentHashMap;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.events.EventPublisher;
import org.openhab.core.thing.Channel; import org.openhab.core.thing.Channel;
import org.openhab.core.thing.ChannelUID; import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.ThingUID; import org.openhab.core.thing.ThingUID;
import org.openhab.core.thing.binding.BaseDynamicStateDescriptionProvider; import org.openhab.core.thing.binding.BaseDynamicStateDescriptionProvider;
import org.openhab.core.thing.events.ThingEventFactory; 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.link.ItemChannelLinkRegistry;
import org.openhab.core.thing.type.DynamicStateDescriptionProvider; import org.openhab.core.thing.type.DynamicStateDescriptionProvider;
import org.openhab.core.types.StateDescription; import org.openhab.core.types.StateDescription;
import org.openhab.core.types.StateDescriptionFragment; 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.Component;
import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -45,6 +49,15 @@ public class DeconzDynamicStateDescriptionProvider extends BaseDynamicStateDescr
private final Map<ChannelUID, StateDescriptionFragment> stateDescriptionFragments = new ConcurrentHashMap<>(); 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 * 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. * 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)) { if (!stateDescriptionFragment.equals(oldStateDescriptionFragment)) {
logger.trace("adding state description for channel {}", channelUID); logger.trace("adding state description for channel {}", channelUID);
stateDescriptionFragments.put(channelUID, stateDescriptionFragment); stateDescriptionFragments.put(channelUID, stateDescriptionFragment);
ItemChannelLinkRegistry itemChannelLinkRegistry = this.itemChannelLinkRegistry; ItemChannelLinkRegistry localItemChannelLinkRegistry = itemChannelLinkRegistry;
postEvent(ThingEventFactory.createChannelDescriptionChangedEvent(channelUID, postEvent(ThingEventFactory.createChannelDescriptionChangedEvent(channelUID,
itemChannelLinkRegistry != null ? itemChannelLinkRegistry.getLinkedItemNames(channelUID) : Set.of(), localItemChannelLinkRegistry != null ? localItemChannelLinkRegistry.getLinkedItemNames(channelUID)
: Set.of(),
stateDescriptionFragment, oldStateDescriptionFragment)); stateDescriptionFragment, oldStateDescriptionFragment));
} }
} }

View File

@ -19,9 +19,11 @@ import java.time.ZoneId;
import java.time.ZoneOffset; import java.time.ZoneOffset;
import java.time.ZonedDateTime; import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.util.Map;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.library.types.DateTimeType; import org.openhab.core.library.types.DateTimeType;
import org.openhab.core.library.types.PercentType; 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 * @param val the value
* @return the corresponding PercentType value * @return the corresponding PercentType value
@ -67,11 +69,11 @@ public class Util {
public static PercentType toPercentType(int val) { public static PercentType toPercentType(int val) {
int scaledValue = (int) Math.ceil(val / BRIGHTNESS_FACTOR); int scaledValue = (int) Math.ceil(val / BRIGHTNESS_FACTOR);
return new PercentType( 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 * @param val the value
* @return the corresponding int 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 * @param timestamp either in zoned date time or local date time format
* @return the corresponding DateTimeType * @return the corresponding DateTimeType
@ -95,4 +97,15 @@ public class Util {
ZoneOffset.UTC, ZoneId.systemDefault())); 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);
}
} }

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -58,7 +58,7 @@ public class BridgeDiscoveryParticipant implements UpnpDiscoveryParticipant {
return null; return null;
} }
URL descriptorURL = device.getIdentity().getDescriptorURL(); URL descriptorURL = device.getIdentity().getDescriptorURL();
String UDN = device.getIdentity().getUdn().getIdentifierString(); String udn = device.getIdentity().getUdn().getIdentifierString();
// Friendly name is like "name (host)" // Friendly name is like "name (host)"
String name = device.getDetails().getFriendlyName(); String name = device.getDetails().getFriendlyName();
@ -75,7 +75,7 @@ public class BridgeDiscoveryParticipant implements UpnpDiscoveryParticipant {
properties.put(CONFIG_HOST, host); properties.put(CONFIG_HOST, host);
properties.put(CONFIG_HTTP_PORT, port); properties.put(CONFIG_HTTP_PORT, port);
properties.put(PROPERTY_UDN, UDN); properties.put(PROPERTY_UDN, udn);
return DiscoveryResultBuilder.create(uid).withProperties(properties).withLabel(name) return DiscoveryResultBuilder.create(uid).withProperties(properties).withLabel(name)
.withRepresentationProperty(PROPERTY_UDN).build(); .withRepresentationProperty(PROPERTY_UDN).build();

View File

@ -14,6 +14,7 @@ package org.openhab.binding.deconz.internal.discovery;
import static org.openhab.binding.deconz.internal.BindingConstants.*; import static org.openhab.binding.deconz.internal.BindingConstants.*;
import java.util.Date;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
@ -74,7 +75,6 @@ public class ThingDiscoveryService extends AbstractDiscoveryService implements D
if (handler != null) { if (handler != null) {
handler.getBridgeFullState().thenAccept(fullState -> { handler.getBridgeFullState().thenAccept(fullState -> {
stopScan(); stopScan();
removeOlderResults(getTimestampOfLastScan());
fullState.ifPresent(state -> { fullState.ifPresent(state -> {
state.sensors.forEach(this::addSensor); state.sensors.forEach(this::addSensor);
state.lights.forEach(this::addLight); 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 @Override
protected void startBackgroundDiscovery() { protected void startBackgroundDiscovery() {
final ScheduledFuture<?> scanningJob = this.scanningJob; final ScheduledFuture<?> scanningJob = this.scanningJob;
@ -127,14 +133,17 @@ public class ThingDiscoveryService extends AbstractDiscoveryService implements D
properties.put(CONFIG_ID, groupId); properties.put(CONFIG_ID, groupId);
switch (groupType) { switch (groupType) {
case LIGHT_GROUP: case LIGHT_GROUP -> thingTypeUID = THING_TYPE_LIGHTGROUP;
thingTypeUID = THING_TYPE_LIGHTGROUP; case LUMINAIRE, LIGHT_SOURCE, ROOM -> {
break; logger.debug("Group {} ({}), type {} ignored.", group.id, group.name, group.type);
default: return;
}
default -> {
logger.debug( logger.debug(
"Found group: {} ({}), type {} but no thing type defined for that type. This should be reported.", "Found group: {} ({}), type {} but no thing type defined for that type. This should be reported.",
group.id, group.name, group.type); group.id, group.name, group.type);
return; return;
}
} }
ThingUID uid = new ThingUID(thingTypeUID, bridgeUID, group.id); ThingUID uid = new ThingUID(thingTypeUID, bridgeUID, group.id);
@ -179,42 +188,24 @@ public class ThingDiscoveryService extends AbstractDiscoveryService implements D
} }
switch (lightType) { switch (lightType) {
case ON_OFF_LIGHT: case ON_OFF_LIGHT, ON_OFF_PLUGIN_UNIT, SMART_PLUG -> thingTypeUID = THING_TYPE_ONOFF_LIGHT;
case ON_OFF_PLUGIN_UNIT: case DIMMABLE_LIGHT, DIMMABLE_PLUGIN_UNIT -> thingTypeUID = THING_TYPE_DIMMABLE_LIGHT;
case SMART_PLUG: case COLOR_TEMPERATURE_LIGHT -> thingTypeUID = THING_TYPE_COLOR_TEMPERATURE_LIGHT;
thingTypeUID = THING_TYPE_ONOFF_LIGHT; case COLOR_DIMMABLE_LIGHT, COLOR_LIGHT -> thingTypeUID = THING_TYPE_COLOR_LIGHT;
break; case EXTENDED_COLOR_LIGHT -> thingTypeUID = THING_TYPE_EXTENDED_COLOR_LIGHT;
case DIMMABLE_LIGHT: case WINDOW_COVERING_DEVICE -> thingTypeUID = THING_TYPE_WINDOW_COVERING;
case DIMMABLE_PLUGIN_UNIT: case WARNING_DEVICE -> thingTypeUID = THING_TYPE_WARNING_DEVICE;
thingTypeUID = THING_TYPE_DIMMABLE_LIGHT; case DOORLOCK -> thingTypeUID = THING_TYPE_DOORLOCK;
break; case CONFIGURATION_TOOL -> {
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:
// ignore configuration tool device // ignore configuration tool device
return; return;
default: }
default -> {
logger.debug( logger.debug(
"Found light: {} ({}), type {} but no thing type defined for that type. This should be reported.", "Found light: {} ({}), type {} but no thing type defined for that type. This should be reported.",
light.modelid, light.name, light.type); light.modelid, light.name, light.type);
return; return;
}
} }
ThingUID uid = new ThingUID(thingTypeUID, bridgeUID, light.uniqueid.replaceAll("[^a-z0-9\\[\\]]", "")); 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 } else if (sensor.type.contains("LightLevel")) { // ZHALightLevel
thingTypeUID = THING_TYPE_LIGHT_SENSOR; thingTypeUID = THING_TYPE_LIGHT_SENSOR;
} else if (sensor.type.contains("ZHAAirQuality")) { // ZHAAirQuality
thingTypeUID = THING_TYPE_AIRQUALITY_SENSOR;
} else if (sensor.type.contains("ZHATemperature")) { // ZHATemperature } else if (sensor.type.contains("ZHATemperature")) { // ZHATemperature
thingTypeUID = THING_TYPE_TEMPERATURE_SENSOR; thingTypeUID = THING_TYPE_TEMPERATURE_SENSOR;
} else if (sensor.type.contains("ZHAHumidity")) { // ZHAHumidity } else if (sensor.type.contains("ZHAHumidity")) { // ZHAHumidity
@ -279,10 +272,10 @@ public class ThingDiscoveryService extends AbstractDiscoveryService implements D
thingTypeUID = THING_TYPE_VIBRATION_SENSOR; // ZHAVibration thingTypeUID = THING_TYPE_VIBRATION_SENSOR; // ZHAVibration
} else if (sensor.type.contains("ZHABattery")) { } else if (sensor.type.contains("ZHABattery")) {
thingTypeUID = THING_TYPE_BATTERY_SENSOR; // 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")) { } else if (sensor.type.contains("ZHAThermostat")) {
thingTypeUID = THING_TYPE_THERMOSTAT; // ZHAThermostat thingTypeUID = THING_TYPE_THERMOSTAT; // ZHAThermostat
} else if (sensor.type.contains("ZHAAirQuality")) {
thingTypeUID = THING_TYPE_AIRQUALITY_SENSOR;
} else { } else {
logger.debug("Unknown type {}", sensor.type); logger.debug("Unknown type {}", sensor.type);
return; return;
@ -316,6 +309,7 @@ public class ThingDiscoveryService extends AbstractDiscoveryService implements D
@Override @Override
public void deactivate() { public void deactivate() {
removeOlderResults(new Date().getTime());
super.deactivate(); super.deactivate();
} }
} }

View File

@ -25,11 +25,15 @@ import org.openhab.binding.deconz.internal.types.ResourceType;
@NonNullByDefault @NonNullByDefault
public class DeconzBaseMessage { public class DeconzBaseMessage {
// For websocket change events // For websocket change events
public String e = ""; // "changed" public String e = ""; // "changed", "scene-called"
public ResourceType r = ResourceType.UNKNOWN; // "sensors" public ResourceType r = ResourceType.UNKNOWN; // "sensors"
public String t = ""; // "event" public String t = ""; // "event"
public String id = ""; // "3" public String id = ""; // "3"
// for scene-recall
public String gid = "";
public String scid = "";
// for rest API // for rest API
public String manufacturername = ""; public String manufacturername = "";
public String modelid = ""; public String modelid = "";

View File

@ -38,6 +38,23 @@ public class GroupAction {
public @Nullable Integer colorloopspeed; public @Nullable Integer colorloopspeed;
public @Nullable Integer transitiontime; 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 @Override
public String toString() { public String toString() {
return "GroupAction{on=" + on + ", toggle=" + toggle + ", bri=" + bri + ", hue=" + hue + ", sat=" + sat return "GroupAction{on=" + on + ", toggle=" + toggle + ", bri=" + bri + ", hue=" + hue + ", sat=" + sat

View File

@ -14,6 +14,8 @@ package org.openhab.binding.deconz.internal.dto;
import org.eclipse.jdt.annotation.NonNullByDefault; 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. * The {@link GroupState} is send by the websocket connection as well as the Rest API.
* It is part of a {@link GroupMessage}. * It is part of a {@link GroupMessage}.
@ -22,11 +24,13 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
*/ */
@NonNullByDefault @NonNullByDefault
public class GroupState { public class GroupState {
public boolean all_on; @SerializedName(value = "all_on")
public boolean any_on; public boolean allOn;
@SerializedName(value = "any_on")
public boolean anyOn;
@Override @Override
public String toString() { public String toString() {
return "GroupState{" + "all_on=" + all_on + ", any_on=" + any_on + '}'; return "GroupState{" + "all_on=" + allOn + ", any_on=" + anyOn + '}';
} }
} }

View File

@ -44,6 +44,11 @@ public class LightState {
public @Nullable Integer ct; public @Nullable Integer ct;
public double @Nullable [] xy; public double @Nullable [] xy;
// for window covering
public @Nullable Boolean open;
public @Nullable Boolean stop;
public @Nullable Integer lift;
public @Nullable Integer transitiontime; public @Nullable Integer transitiontime;
/** /**

View File

@ -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;
}
}

View File

@ -35,10 +35,13 @@ public class SensorConfig {
public @Nullable Integer heatsetpoint; public @Nullable Integer heatsetpoint;
public @Nullable ThermostatMode mode; public @Nullable ThermostatMode mode;
public @Nullable Integer offset; public @Nullable Integer offset;
public @Nullable Boolean locked;
public @Nullable Boolean externalwindowopen;
@Override @Override
public String toString() { public String toString() {
return "SensorConfig{" + "on=" + on + ", reachable=" + reachable + ", battery=" + battery + ", temperature=" 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 + "}";
} }
} }

View File

@ -37,9 +37,9 @@ public class SensorState {
/** Light sensors provide a lux value. */ /** Light sensors provide a lux value. */
public @Nullable Integer lux; public @Nullable Integer lux;
/** Temperature sensors provide a degrees value. */ /** Temperature sensors provide a degrees value. */
public @Nullable Float temperature; public @Nullable Double temperature;
/** Humidity sensors provide a percent value. */ /** Humidity sensors provide a percent value. */
public @Nullable Float humidity; public @Nullable Double humidity;
/** OpenClose sensors provide a boolean value. */ /** OpenClose sensors provide a boolean value. */
public @Nullable Boolean open; public @Nullable Boolean open;
/** fire sensors provide a boolean value. */ /** fire sensors provide a boolean value. */
@ -54,29 +54,23 @@ public class SensorState {
public @Nullable Boolean vibration; public @Nullable Boolean vibration;
/** carbonmonoxide sensors provide a boolean value. */ /** carbonmonoxide sensors provide a boolean value. */
public @Nullable Boolean carbonmonoxide; 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. */ /** Pressure sensors provide a hPa value. */
public @Nullable Integer pressure; public @Nullable Integer pressure;
/** Presence sensors provide this boolean. */ /** Presence sensors provide this boolean. */
public @Nullable Boolean presence; public @Nullable Boolean presence;
/** Power sensors provide this value in Watts. */ /** Power sensors provide this value in Watts. */
public @Nullable Float power; public @Nullable Double power;
/** Batttery sensors provide this value */ /** Batttery sensors provide this value */
public @Nullable Integer battery; public @Nullable Integer battery;
/** /** Consumption sensors provide this value in Watts/hour. */
* Some battery sensors (especially Tuya driven devices) provide this boolean
* instead of battery level
*/
public @Nullable Boolean lowbattery; public @Nullable Boolean lowbattery;
/** Consumption sensors provide this value in Watts/hour. */ /** 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. */ /** Power sensors provide this value in Volt. */
public @Nullable Float voltage; public @Nullable Double voltage;
/** Power sensors provide this value in Milliampere. */ /** 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. */ /** Light sensors and the daylight sensor provide a status integer that can have various semantics. */
public @Nullable Integer status; public @Nullable Integer status;
/** Switches provide this value. */ /** Switches provide this value. */
@ -85,6 +79,11 @@ public class SensorState {
public @Nullable Integer gesture; public @Nullable Integer gesture;
/** Thermostat may provide this value. */ /** Thermostat may provide this value. */
public @Nullable Integer valve; 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 */ /** Thermostats may provide this value */
public @Nullable String windowopen; public @Nullable String windowopen;
/** deCONZ sends a last update string with every event. */ /** 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=" return "SensorState{" + "dark=" + dark + ", daylight=" + daylight + ", lightlevel=" + lightlevel + ", lux="
+ lux + ", temperature=" + temperature + ", humidity=" + humidity + ", open=" + open + ", fire=" + fire + lux + ", temperature=" + temperature + ", humidity=" + humidity + ", open=" + open + ", fire=" + fire
+ ", water=" + water + ", alarm=" + alarm + ", tampered=" + tampered + ", vibration=" + vibration + ", water=" + water + ", alarm=" + alarm + ", tampered=" + tampered + ", vibration=" + vibration
+ ", carbonmonoxide=" + carbonmonoxide + ", airquality=" + airquality + ", airqualityppb=" + ", carbonmonoxide=" + carbonmonoxide + ", pressure=" + pressure + ", presence=" + presence
+ airqualityppb + ", pressure=" + pressure + ", presence=" + presence + ", power=" + power + ", power=" + power + ", battery=" + battery + ", lowbattery=" + lowbattery + ", consumption="
+ ", battery=" + battery + ", consumption=" + consumption + ", voltage=" + voltage + ", current=" + consumption + ", voltage=" + voltage + ", current=" + current + ", status=" + status
+ current + ", status=" + status + ", buttonevent=" + buttonevent + ", gesture=" + gesture + ", valve=" + ", buttonevent=" + buttonevent + ", gesture=" + gesture + ", valve=" + valve + ", airquality='"
+ valve + ", windowopen='" + windowopen + '\'' + ", lastupdated='" + lastupdated + '\'' + ", xy=" + airquality + "'" + ", airqualityppb=" + airqualityppb + ", windowopen='" + windowopen + "'"
+ Arrays.toString(xy) + '}'; + ", lastupdated='" + lastupdated + "'" + ", xy=" + Arrays.toString(xy) + "}";
} }
} }

View File

@ -26,4 +26,6 @@ public class ThermostatUpdateConfig {
public @Nullable Integer heatsetpoint; public @Nullable Integer heatsetpoint;
public @Nullable ThermostatMode mode; public @Nullable ThermostatMode mode;
public @Nullable Integer offset; public @Nullable Integer offset;
public @Nullable Boolean locked;
public @Nullable Boolean externalwindowopen;
} }

View File

@ -13,19 +13,26 @@
package org.openhab.binding.deconz.internal.handler; package org.openhab.binding.deconz.internal.handler;
import static org.openhab.binding.deconz.internal.BindingConstants.*; 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.ScheduledFuture;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.function.Consumer; 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.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable; 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.dto.DeconzBaseMessage;
import org.openhab.binding.deconz.internal.netutils.WebSocketConnection; import org.openhab.binding.deconz.internal.netutils.WebSocketConnection;
import org.openhab.binding.deconz.internal.netutils.WebSocketMessageListener; import org.openhab.binding.deconz.internal.netutils.WebSocketMessageListener;
import org.openhab.binding.deconz.internal.types.ResourceType; 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.Bridge;
import org.openhab.core.thing.Channel; import org.openhab.core.thing.Channel;
import org.openhab.core.thing.ChannelUID; 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.ThingStatusInfo;
import org.openhab.core.thing.binding.BaseThingHandler; import org.openhab.core.thing.binding.BaseThingHandler;
import org.openhab.core.thing.binding.ThingHandlerCallback; 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.ChannelKind;
import org.openhab.core.thing.type.ChannelTypeUID; import org.openhab.core.thing.type.ChannelTypeUID;
import org.openhab.core.types.Command; import org.openhab.core.types.Command;
@ -58,7 +66,9 @@ public abstract class DeconzBaseThingHandler extends BaseThingHandler implements
protected final ResourceType resourceType; protected final ResourceType resourceType;
protected ThingConfig config = new ThingConfig(); protected ThingConfig config = new ThingConfig();
protected final Gson gson; protected final Gson gson;
private @Nullable ScheduledFuture<?> initializationJob; private @Nullable ScheduledFuture<?> initializationJob;
private @Nullable ScheduledFuture<?> lastSeenPollingJob;
protected @Nullable WebSocketConnection connection; protected @Nullable WebSocketConnection connection;
public DeconzBaseThingHandler(Thing thing, Gson gson, ResourceType resourceType) { 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() { private void stopInitializationJob() {
ScheduledFuture<?> future = initializationJob; ScheduledFuture<?> future = initializationJob;
@ -78,10 +88,14 @@ public abstract class DeconzBaseThingHandler extends BaseThingHandler implements
} }
} }
private void registerListener() { /**
WebSocketConnection conn = connection; * Stops the last_seen polling
if (conn != null) { */
conn.registerListener(resourceType, config.id, this); 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; return;
} }
final WebSocketConnection webSocketConnection = bridgeHandler.getWebsocketConnection();
this.connection = webSocketConnection;
updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.NONE); updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.NONE);
// Real-time data // Real-time data
registerListener(); WebSocketConnection socketConnection = bridgeHandler.getWebSocketConnection();
this.connection = socketConnection;
socketConnection.registerListener(resourceType, config.id, this);
// get initial values // get initial values
requestState(this::processStateResponse); requestState(this::processStateResponse);
@ -145,7 +158,7 @@ public abstract class DeconzBaseThingHandler extends BaseThingHandler implements
protected abstract void processStateResponse(DeconzBaseMessage stateResponse); 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) { protected void requestState(Consumer<DeconzBaseMessage> processor) {
DeconzBridgeHandler bridgeHandler = getBridgeHandler(); 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 * sends a command to the bridge with the default command URL
* *
* @param object must be serializable and contain the command * @param object must be serializable and contain the command
* @param originalCommand the original openHAB command (used for logging purposes) * @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) * @param acceptProcessing additional processing after the command was successfully send (might be null)
*/ */
protected void sendCommand(@Nullable Object object, Command originalCommand, ChannelUID channelUID, 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 object must be serializable and contain the command
* @param originalCommand the original openHAB command (used for logging purposes) * @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 commandUrl the command URL
* @param acceptProcessing additional processing after the command was successfully send (might be null) * @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) { if (bridgeHandler == null) {
return; return;
} }
String endpoint = Stream.of(resourceType.getIdentifier(), config.id, commandUrl) String endpoint = String.join("/", resourceType.getIdentifier(), config.id, commandUrl);
.collect(Collectors.joining("/"));
bridgeHandler.sendObject(endpoint, object).thenAccept(v -> { bridgeHandler.sendObject(endpoint, object, HttpMethod.PUT).thenAccept(v -> {
if (acceptProcessing != null) { if (acceptProcessing != null) {
acceptProcessing.run(); 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 @Override
public void dispose() { public void dispose() {
stopInitializationJob(); stopInitializationJob();
stopLastSeenPollingJob();
unregisterListener(); unregisterListener();
super.dispose(); super.dispose();
} }
@ -229,29 +339,55 @@ public abstract class DeconzBaseThingHandler extends BaseThingHandler implements
} }
} }
protected void createChannel(String channelId, ChannelKind kind) { protected void updateStringChannel(ChannelUID channelUID, @Nullable String value) {
if (thing.getChannel(channelId) != null) { if (value == null) {
// channel already exists, no update necessary
return; return;
} }
updateState(channelUID, new StringType(value));
}
ThingHandlerCallback callback = getCallback(); protected void updateSwitchChannel(ChannelUID channelUID, @Nullable Boolean value) {
if (callback != null) { if (value == null) {
ChannelUID channelUID = new ChannelUID(thing.getUID(), channelId); return;
ChannelTypeUID channelTypeUID; }
switch (channelId) { updateState(channelUID, OnOffType.from(value));
case CHANNEL_BATTERY_LEVEL: }
channelTypeUID = new ChannelTypeUID("system:battery-level");
break; protected void updateDecimalTypeChannel(ChannelUID channelUID, @Nullable Number value) {
case CHANNEL_BATTERY_LOW: if (value == null) {
channelTypeUID = new ChannelTypeUID("system:low-battery"); return;
break; }
default: updateState(channelUID, new DecimalType(value.longValue()));
channelTypeUID = new ChannelTypeUID(BINDING_ID, channelId); }
break;
} protected void updateQuantityTypeChannel(ChannelUID channelUID, @Nullable Number value, Unit<?> unit) {
Channel channel = callback.createChannelBuilder(channelUID, channelTypeUID).withKind(kind).build(); updateQuantityTypeChannel(channelUID, value, unit, 1.0);
updateThing(editThing().withChannel(channel).build()); }
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);
} }
} }
} }

View File

@ -26,7 +26,8 @@ public class DeconzBridgeConfig {
public int httpPort = 80; public int httpPort = 80;
public int port = 0; public int port = 0;
public @Nullable String apikey; public @Nullable String apikey;
int timeout = 2000; public int timeout = 2000;
public int websocketTimeout = 120;
public String getHostWithoutPort() { public String getHostWithoutPort() {
String hostWithoutPort = host; String hostWithoutPort = host;

View File

@ -17,7 +17,6 @@ import static org.openhab.binding.deconz.internal.Util.buildUrl;
import java.net.SocketTimeoutException; import java.net.SocketTimeoutException;
import java.util.Collection; import java.util.Collection;
import java.util.Collections;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.Optional; import java.util.Optional;
@ -30,6 +29,8 @@ import java.util.concurrent.TimeoutException;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable; 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.discovery.ThingDiscoveryService;
import org.openhab.binding.deconz.internal.dto.ApiKeyMessage; import org.openhab.binding.deconz.internal.dto.ApiKeyMessage;
import org.openhab.binding.deconz.internal.dto.BridgeFullState; 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.io.net.http.WebSocketFactory;
import org.openhab.core.thing.Bridge; import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.ChannelUID; import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus; import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail; import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.ThingTypeUID; import org.openhab.core.thing.ThingTypeUID;
@ -65,35 +67,43 @@ import com.google.gson.Gson;
*/ */
@NonNullByDefault @NonNullByDefault
public class DeconzBridgeHandler extends BaseBridgeHandler implements WebSocketConnectionListener { 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 Logger logger = LoggerFactory.getLogger(DeconzBridgeHandler.class);
private final WebSocketConnection websocket;
private final AsyncHttpClient http; private final AsyncHttpClient http;
private final WebSocketFactory webSocketFactory;
private DeconzBridgeConfig config = new DeconzBridgeConfig(); private DeconzBridgeConfig config = new DeconzBridgeConfig();
private final Gson gson; private final Gson gson;
private @Nullable ScheduledFuture<?> scheduledFuture; private @Nullable ScheduledFuture<?> connectionJob;
private int websocketPort = 0; private int websocketPort = 0;
/** Prevent a dispose/init cycle while this flag is set. Use for property updates */ /** Prevent a dispose/init cycle while this flag is set. Use for property updates */
private boolean ignoreConfigurationUpdate; private boolean ignoreConfigurationUpdate;
private boolean thingDisposing = false; private boolean thingDisposing = false;
private WebSocketConnection webSocketConnection;
private final ExpiringCacheAsync<Optional<BridgeFullState>> fullStateCache = new ExpiringCacheAsync<>(1000); private final ExpiringCacheAsync<Optional<BridgeFullState>> fullStateCache = new ExpiringCacheAsync<>(1000);
/** The poll frequency for the API Key verification */ /** The poll frequency for the API Key verification */
private static final int POLL_FREQUENCY_SEC = 10; private static final int POLL_FREQUENCY_SEC = 10;
private boolean ignoreConnectionLost = true;
public DeconzBridgeHandler(Bridge thing, WebSocketFactory webSocketFactory, AsyncHttpClient http, Gson gson) { public DeconzBridgeHandler(Bridge thing, WebSocketFactory webSocketFactory, AsyncHttpClient http, Gson gson) {
super(thing); super(thing);
this.http = http; this.http = http;
this.gson = gson; this.gson = gson;
this.webSocketFactory = webSocketFactory;
this.webSocketConnection = createNewWebSocketConnection();
}
private WebSocketConnection createNewWebSocketConnection() {
String websocketID = ThingWebClientUtil.buildWebClientConsumerName(thing.getUID(), null); 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 @Override
public Collection<Class<? extends ThingHandlerService>> getServices() { public Collection<Class<? extends ThingHandlerService>> getServices() {
return Set.of(ThingDiscoveryService.class); return Set.of(ThingDiscoveryService.class, BridgeActions.class);
} }
@Override @Override
@ -107,14 +117,23 @@ public class DeconzBridgeHandler extends BaseBridgeHandler implements WebSocketC
public void handleCommand(ChannelUID channelUID, Command command) { 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 * Stops the API request or websocket reconnect timer
*/ */
private void stopTimer() { private void stopTimer() {
ScheduledFuture<?> future = scheduledFuture; ScheduledFuture<?> future = connectionJob;
if (future != null) { if (future != null) {
future.cancel(true); future.cancel(false);
scheduledFuture = null; connectionJob = null;
} }
} }
@ -132,7 +151,7 @@ public class DeconzBridgeHandler extends BaseBridgeHandler implements WebSocketC
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING, updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING,
"Allow authentication for 3rd party apps. Trying again in " + POLL_FREQUENCY_SEC + " seconds"); "Allow authentication for 3rd party apps. Trying again in " + POLL_FREQUENCY_SEC + " seconds");
stopTimer(); 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) { } else if (r.getResponseCode() == 200) {
ApiKeyMessage[] response = Objects.requireNonNull(gson.fromJson(r.getBody(), ApiKeyMessage[].class)); ApiKeyMessage[] response = Objects.requireNonNull(gson.fromJson(r.getBody(), ApiKeyMessage[].class));
if (response.length == 0) { if (response.length == 0) {
@ -171,7 +190,7 @@ public class DeconzBridgeHandler extends BaseBridgeHandler implements WebSocketC
String url = buildUrl(config.getHostWithoutPort(), config.httpPort, config.apikey); String url = buildUrl(config.getHostWithoutPort(), config.httpPort, config.apikey);
return http.get(url, config.timeout).thenApply(r -> { return http.get(url, config.timeout).thenApply(r -> {
if (r.getResponseCode() == 403) { if (r.getResponseCode() == 403) {
return Optional.<BridgeFullState> empty(); return Optional.ofNullable((BridgeFullState) null);
} else if (r.getResponseCode() == 200) { } else if (r.getResponseCode() == 200) {
return Optional.ofNullable(gson.fromJson(r.getBody(), BridgeFullState.class)); return Optional.ofNullable(gson.fromJson(r.getBody(), BridgeFullState.class));
} else { } else {
@ -225,11 +244,11 @@ public class DeconzBridgeHandler extends BaseBridgeHandler implements WebSocketC
// Use requested websocket port if no specific port is given // Use requested websocket port if no specific port is given
websocketPort = config.port == 0 ? state.config.websocketport : config.port; websocketPort = config.port == 0 ? state.config.websocketport : config.port;
startWebsocket(); startWebSocketConnection();
}, () -> { }, () -> {
// initial response was empty, re-trying in POLL_FREQUENCY_SEC seconds // initial response was empty, re-trying in POLL_FREQUENCY_SEC seconds
if (!thingDisposing) { if (!thingDisposing) {
scheduledFuture = scheduler.schedule(this::initializeBridgeState, POLL_FREQUENCY_SEC, TimeUnit.SECONDS); connectionJob = scheduler.schedule(this::initializeBridgeState, POLL_FREQUENCY_SEC, TimeUnit.SECONDS);
} }
})).exceptionally(e -> { })).exceptionally(e -> {
if (e != null) { 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); logger.warn("Initial full state request or result parsing failed", e);
if (!thingDisposing) { if (!thingDisposing) {
scheduledFuture = scheduler.schedule(this::initializeBridgeState, POLL_FREQUENCY_SEC, TimeUnit.SECONDS); connectionJob = scheduler.schedule(this::initializeBridgeState, POLL_FREQUENCY_SEC, TimeUnit.SECONDS);
} }
return null; return null;
}); });
@ -249,15 +268,16 @@ public class DeconzBridgeHandler extends BaseBridgeHandler implements WebSocketC
* Starts the websocket connection. * Starts the websocket connection.
* {@link #initializeBridgeState} need to be called first to obtain the websocket port. * {@link #initializeBridgeState} need to be called first to obtain the websocket port.
*/ */
private void startWebsocket() { private void startWebSocketConnection() {
if (websocket.isConnected() || websocketPort == 0 || thingDisposing) { ignoreConnectionLost = false;
if (webSocketConnection.isConnected() || websocketPort == 0 || thingDisposing) {
return; return;
} }
stopTimer(); 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()); logger.debug("Start initializing bridge {}", thing.getUID());
thingDisposing = false; thingDisposing = false;
config = getConfigAs(DeconzBridgeConfig.class); config = getConfigAs(DeconzBridgeConfig.class);
webSocketConnection.setWatchdogInterval(config.websocketTimeout);
updateStatus(ThingStatus.UNKNOWN);
if (config.apikey == null) { if (config.apikey == null) {
requestApiKey(); requestApiKey();
} else { } else {
@ -292,29 +314,37 @@ public class DeconzBridgeHandler extends BaseBridgeHandler implements WebSocketC
public void dispose() { public void dispose() {
thingDisposing = true; thingDisposing = true;
stopTimer(); stopTimer();
websocket.close(); webSocketConnection.dispose();
} }
@Override @Override
public void connectionEstablished() { public void webSocketConnectionEstablished() {
stopTimer(); stopTimer();
updateStatus(ThingStatus.ONLINE); updateStatus(ThingStatus.ONLINE);
} }
@Override @Override
public void connectionLost(String reason) { public void webSocketConnectionLost(String reason) {
if (ignoreConnectionLost) {
return;
}
ignoreConnectionLost = true;
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, reason); updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, reason);
stopTimer(); 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 // 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. * Return the websocket connection.
*/ */
public WebSocketConnection getWebsocketConnection() { public WebSocketConnection getWebSocketConnection() {
return websocket; return webSocketConnection;
} }
/** /**
@ -322,13 +352,23 @@ public class DeconzBridgeHandler extends BaseBridgeHandler implements WebSocketC
* *
* @param endPoint the endpoint (e.g. "lights/2/state") * @param endPoint the endpoint (e.g. "lights/2/state")
* @param object the object (or null if no object) * @param object the object (or null if no object)
* @param httpMethod the HTTP Method
* @return CompletableFuture of the result * @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 json = object == null ? null : gson.toJson(object);
String url = buildUrl(config.host, config.httpPort, config.apikey, endPoint); String url = buildUrl(config.host, config.httpPort, config.apikey, endPoint);
logger.trace("Sending {} via {}", json, url); logger.trace("Sending {} via {} to {}", json, httpMethod, url);
return http.put(url, json, config.timeout); if (httpMethod == HttpMethod.PUT) {
return http.put(url, json, config.timeout);
} else if (httpMethod == HttpMethod.POST) {
return http.post(url, json, config.timeout);
} else if (httpMethod == HttpMethod.DELETE) {
return http.delete(url, config.timeout);
}
return CompletableFuture.failedFuture(new IllegalArgumentException("Unknown HTTP Method"));
} }
} }

View File

@ -13,14 +13,19 @@
package org.openhab.binding.deconz.internal.handler; package org.openhab.binding.deconz.internal.handler;
import static org.openhab.binding.deconz.internal.BindingConstants.*; 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.Map;
import java.util.Set; import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.deconz.internal.DeconzDynamicCommandDescriptionProvider; import org.openhab.binding.deconz.internal.DeconzDynamicCommandDescriptionProvider;
import org.openhab.binding.deconz.internal.Util; 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.DeconzBaseMessage;
import org.openhab.binding.deconz.internal.dto.GroupAction; import org.openhab.binding.deconz.internal.dto.GroupAction;
import org.openhab.binding.deconz.internal.dto.GroupMessage; 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.OnOffType;
import org.openhab.core.library.types.PercentType; import org.openhab.core.library.types.PercentType;
import org.openhab.core.library.types.StringType; import org.openhab.core.library.types.StringType;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.ChannelUID; import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing; import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus; import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingTypeUID; import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.binding.ThingHandlerService;
import org.openhab.core.types.Command; import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType; 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.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -81,71 +91,83 @@ public class GroupThingHandler extends DeconzBaseThingHandler {
GroupAction newGroupAction = new GroupAction(); GroupAction newGroupAction = new GroupAction();
switch (channelId) { switch (channelId) {
case CHANNEL_ALL_ON: case CHANNEL_ALL_ON, CHANNEL_ANY_ON -> {
case CHANNEL_ANY_ON:
if (command instanceof RefreshType) { if (command instanceof RefreshType) {
valueUpdated(channelUID.getId(), groupStateCache); valueUpdated(channelUID, groupStateCache);
return; return;
} }
break; }
case CHANNEL_ALERT: case CHANNEL_ALERT -> {
if (command instanceof StringType) { if (command instanceof StringType) {
newGroupAction.alert = command.toString(); newGroupAction.alert = command.toString();
} else { } else {
return; return;
} }
break; }
case CHANNEL_COLOR: case CHANNEL_COLOR -> {
if (command instanceof HSBType) { if (command instanceof OnOffType) {
HSBType hsbCommand = (HSBType) command; 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 // 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. // 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)) { if ("hs".equals(colorMode)) {
newGroupAction.hue = (int) (hsbCommand.getHue().doubleValue() * HUE_FACTOR); newGroupAction.hue = (int) (hsbCommand.getHue().doubleValue() * HUE_FACTOR);
newGroupAction.sat = Util.fromPercentType(hsbCommand.getSaturation()); newGroupAction.sat = Util.fromPercentType(hsbCommand.getSaturation());
newGroupAction.bri = Util.fromPercentType(hsbCommand.getBrightness());
} else { } else {
PercentType[] xy = hsbCommand.toXY(); double[] xy = ColorUtil.hsbToXY(hsbCommand);
if (xy.length < 2) { newGroupAction.xy = new double[] { xy[0], xy[1] };
logger.warn("Failed to convert {} to xy-values", command); newGroupAction.bri = (int) (xy[2] * BRIGHTNESS_MAX);
}
newGroupAction.xy = new double[] { xy[0].doubleValue() / 100.0, xy[1].doubleValue() / 100.0 };
} }
} else if (command instanceof PercentType) { } else if (command instanceof PercentType) {
newGroupAction.bri = Util.fromPercentType((PercentType) command); newGroupAction.bri = Util.fromPercentType((PercentType) command);
} else if (command instanceof DecimalType) { } else if (command instanceof DecimalType) {
newGroupAction.bri = ((DecimalType) command).intValue(); newGroupAction.bri = ((DecimalType) command).intValue();
} else if (command instanceof OnOffType) {
newGroupAction.on = OnOffType.ON.equals(command);
} else { } else {
return; return;
} }
break;
case CHANNEL_COLOR_TEMPERATURE: // send on/off state together with brightness if not already set or unknown
if (command instanceof DecimalType) { Integer newBri = newGroupAction.bri;
int miredValue = Util.kelvinToMired(((DecimalType) command).intValue()); if (newBri != null) {
newGroupAction.ct = Util.constrainToRange(miredValue, ZCL_CT_MIN, ZCL_CT_MAX); newGroupAction.on = (newBri > 0);
} else {
return;
} }
break; Double transitiontime = config.transitiontime;
case CHANNEL_SCENE: 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) { if (command instanceof StringType) {
String sceneId = scenes.get(command.toString()); getIdFromSceneName(command.toString())
if (sceneId != null) { .thenAccept(id -> sendCommand(null, command, channelUID, "scenes/" + id + "/recall", null))
sendCommand(null, command, channelUID, "scenes/" + sceneId + "/recall", null); .exceptionally(e -> {
} else { logger.debug("Ignoring command {} for {}, scene is not found in available scenes {}.",
logger.debug("Ignoring command {} for {}, scene is not found in available scenes: {}", command, command, channelUID, scenes);
channelUID, scenes); return null;
} });
} }
return; return;
default: }
default -> {
// no supported command
return; return;
}
} }
Integer bri = newGroupAction.bri; Boolean newOn = newGroupAction.on;
if (bri != null) { if (newOn != null && !newOn) {
newGroupAction.on = (bri > 0); // 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); sendCommand(newGroupAction, command, channelUID, null);
@ -153,38 +175,25 @@ public class GroupThingHandler extends DeconzBaseThingHandler {
@Override @Override
protected void processStateResponse(DeconzBaseMessage stateResponse) { protected void processStateResponse(DeconzBaseMessage stateResponse) {
if (stateResponse instanceof GroupMessage) { scenes = processScenes(stateResponse);
GroupMessage groupMessage = (GroupMessage) stateResponse; messageReceived(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);
} }
private void valueUpdated(String channelId, GroupState newState) { private void valueUpdated(ChannelUID channelUID, GroupState newState) {
switch (channelId) { switch (channelUID.getId()) {
case CHANNEL_ALL_ON: case CHANNEL_ALL_ON -> updateSwitchChannel(channelUID, newState.allOn);
updateState(channelId, OnOffType.from(newState.all_on)); case CHANNEL_ANY_ON -> updateSwitchChannel(channelUID, newState.anyOn);
break;
case CHANNEL_ANY_ON:
updateState(channelId, OnOffType.from(newState.any_on));
break;
default:
} }
} }
@Override @Override
public void messageReceived(String sensorID, DeconzBaseMessage message) { public void messageReceived(DeconzBaseMessage message) {
if (message instanceof GroupMessage) { if (message instanceof GroupMessage groupMessage) {
GroupMessage groupMessage = (GroupMessage) message;
logger.trace("{} received {}", thing.getUID(), groupMessage); logger.trace("{} received {}", thing.getUID(), groupMessage);
GroupState groupState = groupMessage.state; GroupState groupState = groupMessage.state;
if (groupState != null) { if (groupState != null) {
updateStatus(ThingStatus.ONLINE); 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; groupStateCache = groupState;
} }
GroupAction groupAction = groupMessage.action; 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);
}
} }

View 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.StringType;
import org.openhab.core.library.types.UpDownType; import org.openhab.core.library.types.UpDownType;
import org.openhab.core.library.unit.Units; import org.openhab.core.library.unit.Units;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.ChannelUID; import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing; import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus; import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail; import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.ThingTypeUID; 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.thing.type.ChannelKind;
import org.openhab.core.types.Command; import org.openhab.core.types.Command;
import org.openhab.core.types.CommandOption; import org.openhab.core.types.CommandOption;
import org.openhab.core.types.RefreshType; import org.openhab.core.types.RefreshType;
import org.openhab.core.types.StateDescriptionFragment; import org.openhab.core.types.StateDescriptionFragment;
import org.openhab.core.types.StateDescriptionFragmentBuilder; 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.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -94,8 +96,7 @@ public class LightThingHandler extends DeconzBaseThingHandler {
*/ */
private LightState lightStateCache = new LightState(); private LightState lightStateCache = new LightState();
private LightState lastCommand = new LightState(); private LightState lastCommand = new LightState();
@Nullable private @Nullable Integer onTime = null; // in 0.1s
private Integer onTime = null; // in 0.1s
private String colorMode = ""; private String colorMode = "";
// set defaults, we can override them later if we receive better values // set defaults, we can override them later if we receive better values
@ -139,8 +140,8 @@ public class LightThingHandler extends DeconzBaseThingHandler {
@Override @Override
public void handleCommand(ChannelUID channelUID, Command command) { public void handleCommand(ChannelUID channelUID, Command command) {
if (channelUID.getId().equals(CHANNEL_ONTIME)) { if (channelUID.getId().equals(CHANNEL_ONTIME)) {
if (command instanceof QuantityType<?>) { if (command instanceof QuantityType<?> quantity) {
QuantityType<?> onTimeSeconds = ((QuantityType<?>) command).toUnit(Units.SECOND); QuantityType<?> onTimeSeconds = quantity.toUnit(Units.SECOND);
if (onTimeSeconds != null) { if (onTimeSeconds != null) {
onTime = 10 * onTimeSeconds.intValue(); onTime = 10 * onTimeSeconds.intValue();
} else { } else {
@ -152,7 +153,7 @@ public class LightThingHandler extends DeconzBaseThingHandler {
} }
if (command instanceof RefreshType) { if (command instanceof RefreshType) {
valueUpdated(channelUID.getId(), lightStateCache); valueUpdated(channelUID, lightStateCache);
return; return;
} }
@ -161,14 +162,14 @@ public class LightThingHandler extends DeconzBaseThingHandler {
Integer currentBri = lightStateCache.bri; Integer currentBri = lightStateCache.bri;
switch (channelUID.getId()) { switch (channelUID.getId()) {
case CHANNEL_ALERT: case CHANNEL_ALERT -> {
if (command instanceof StringType) { if (command instanceof StringType) {
newLightState.alert = command.toString(); newLightState.alert = command.toString();
} else { } else {
return; return;
} }
break; }
case CHANNEL_EFFECT: case CHANNEL_EFFECT -> {
if (command instanceof StringType) { if (command instanceof StringType) {
// effect command only allowed for lights that are turned on // effect command only allowed for lights that are turned on
newLightState.on = true; newLightState.on = true;
@ -176,25 +177,23 @@ public class LightThingHandler extends DeconzBaseThingHandler {
} else { } else {
return; return;
} }
break; }
case CHANNEL_EFFECT_SPEED: case CHANNEL_EFFECT_SPEED -> {
if (command instanceof DecimalType) { if (command instanceof DecimalType) {
newLightState.on = true; newLightState.on = true;
newLightState.effectSpeed = Util.constrainToRange(((DecimalType) command).intValue(), 0, 10); newLightState.effectSpeed = Util.constrainToRange(((DecimalType) command).intValue(), 0, 10);
} else { } else {
return; return;
} }
break; }
case CHANNEL_SWITCH: case CHANNEL_SWITCH, CHANNEL_LOCK -> {
case CHANNEL_LOCK:
if (command instanceof OnOffType) { if (command instanceof OnOffType) {
newLightState.on = (command == OnOffType.ON); newLightState.on = (command == OnOffType.ON);
} else { } else {
return; return;
} }
break; }
case CHANNEL_BRIGHTNESS: case CHANNEL_BRIGHTNESS, CHANNEL_COLOR -> {
case CHANNEL_COLOR:
if (command instanceof OnOffType) { if (command instanceof OnOffType) {
newLightState.on = (command == OnOffType.ON); newLightState.on = (command == OnOffType.ON);
} else if (command instanceof IncreaseDecreaseType) { } else if (command instanceof IncreaseDecreaseType) {
@ -208,21 +207,18 @@ public class LightThingHandler extends DeconzBaseThingHandler {
newLightState.bri = Util.constrainToRange(oldBri - BRIGHTNESS_DIM_STEP, BRIGHTNESS_MIN, newLightState.bri = Util.constrainToRange(oldBri - BRIGHTNESS_DIM_STEP, BRIGHTNESS_MIN,
BRIGHTNESS_MAX); BRIGHTNESS_MAX);
} }
} else if (command instanceof HSBType) { } else if (command instanceof HSBType hsbCommand) {
HSBType hsbCommand = (HSBType) command;
// XY color is the implicit default: Use XY color mode if i) no color mode is set or ii) if the bulb // 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. // 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)) { if ("hs".equals(colorMode)) {
newLightState.hue = (int) (hsbCommand.getHue().doubleValue() * HUE_FACTOR); newLightState.hue = (int) (hsbCommand.getHue().doubleValue() * HUE_FACTOR);
newLightState.sat = Util.fromPercentType(hsbCommand.getSaturation()); newLightState.sat = Util.fromPercentType(hsbCommand.getSaturation());
newLightState.bri = Util.fromPercentType(hsbCommand.getBrightness());
} else { } else {
PercentType[] xy = hsbCommand.toXY(); double[] xy = ColorUtil.hsbToXY(hsbCommand);
if (xy.length < 2) { newLightState.xy = new double[] { xy[0], xy[1] };
logger.warn("Failed to convert {} to xy-values", command); newLightState.bri = (int) (xy[2] * BRIGHTNESS_MAX);
}
newLightState.xy = new double[] { xy[0].doubleValue() / 100.0, xy[1].doubleValue() / 100.0 };
} }
newLightState.bri = Util.fromPercentType(hsbCommand.getBrightness());
} else if (command instanceof PercentType) { } else if (command instanceof PercentType) {
newLightState.bri = Util.fromPercentType((PercentType) command); newLightState.bri = Util.fromPercentType((PercentType) command);
} else if (command instanceof DecimalType) { } else if (command instanceof DecimalType) {
@ -241,40 +237,34 @@ public class LightThingHandler extends DeconzBaseThingHandler {
if (newBri != null && newBri == 0 && currentOn != null && !currentOn) { if (newBri != null && newBri == 0 && currentOn != null && !currentOn) {
return; return;
} }
Double transitiontime = config.transitiontime; Double transitiontime = config.transitiontime;
if (transitiontime != null) { if (transitiontime != null) {
// value is in 1/10 seconds // value is in 1/10 seconds
newLightState.transitiontime = (int) Math.round(10 * transitiontime); newLightState.transitiontime = (int) Math.round(10 * transitiontime);
} }
break; }
case CHANNEL_COLOR_TEMPERATURE: case CHANNEL_COLOR_TEMPERATURE -> {
if (command instanceof DecimalType) { if (command instanceof DecimalType) {
int miredValue = kelvinToMired(((DecimalType) command).intValue()); int miredValue = kelvinToMired(((DecimalType) command).intValue());
newLightState.ct = constrainToRange(miredValue, ctMin, ctMax); newLightState.ct = constrainToRange(miredValue, ctMin, ctMax);
newLightState.on = true; newLightState.on = true;
} }
break; }
case CHANNEL_POSITION: case CHANNEL_POSITION -> {
if (command instanceof UpDownType) { if (command instanceof UpDownType) {
newLightState.on = (command == UpDownType.DOWN); newLightState.open = (command == UpDownType.UP);
} else if (command == StopMoveType.STOP) { } else if (command == StopMoveType.STOP) {
if (currentOn != null && currentOn && currentBri != null && currentBri <= BRIGHTNESS_MAX) { newLightState.stop = true;
// 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;
}
} else if (command instanceof PercentType) { } else if (command instanceof PercentType) {
newLightState.bri = fromPercentType((PercentType) command); newLightState.lift = ((PercentType) command).intValue();
} else { } else {
return; return;
} }
break; }
default: default -> {
// no supported command // no supported command
return; return;
}
} }
Boolean newOn = newLightState.on; Boolean newOn = newLightState.on;
@ -296,12 +286,10 @@ public class LightThingHandler extends DeconzBaseThingHandler {
@Override @Override
protected void processStateResponse(DeconzBaseMessage stateResponse) { protected void processStateResponse(DeconzBaseMessage stateResponse) {
if (!(stateResponse instanceof LightMessage)) { if (!(stateResponse instanceof LightMessage lightMessage)) {
return; return;
} }
LightMessage lightMessage = (LightMessage) stateResponse;
if (needsPropertyUpdate) { if (needsPropertyUpdate) {
// if we did not receive a ctmin/ctmax, then we probably don't need it // if we did not receive a ctmin/ctmax, then we probably don't need it
needsPropertyUpdate = false; needsPropertyUpdate = false;
@ -316,41 +304,60 @@ public class LightThingHandler extends DeconzBaseThingHandler {
} }
} }
ThingBuilder thingBuilder = editThing();
boolean thingEdited = false;
LightState lightState = lightMessage.state; LightState lightState = lightMessage.state;
if (lightState != null && lightState.effect != null) { if (lightState != null && lightState.effect != null
checkAndUpdateEffectChannels(lightMessage); && checkAndUpdateEffectChannels(thingBuilder, lightMessage)) {
thingEdited = true;
} }
messageReceived(config.id, lightMessage); if (checkLastSeen(thingBuilder, stateResponse.lastseen)) {
thingEdited = true;
}
if (thingEdited) {
updateThing(thingBuilder.build());
}
messageReceived(lightMessage);
} }
private enum EffectLightModel { private enum EffectLightModel {
LIDL_MELINARA, LIDL_MELINARA,
TINT_MUELLER, TINT_MUELLER,
UNKNOWN; UNKNOWN
} }
private void checkAndUpdateEffectChannels(LightMessage lightMessage) { private boolean checkAndUpdateEffectChannels(ThingBuilder thingBuilder, LightMessage lightMessage) {
EffectLightModel model = EffectLightModel.UNKNOWN;
// try to determine which model we have // try to determine which model we have
if (lightMessage.manufacturername.equals("_TZE200_s8gkrkxk")) { EffectLightModel model = switch (lightMessage.manufacturername) {
// the LIDL Melinara string does not report a proper model name case "_TZE200_s8gkrkxk" -> EffectLightModel.LIDL_MELINARA;
model = EffectLightModel.LIDL_MELINARA; case "MLI" -> EffectLightModel.TINT_MUELLER;
} else if (lightMessage.manufacturername.equals("MLI")) { default -> EffectLightModel.UNKNOWN;
model = EffectLightModel.TINT_MUELLER; };
} else { if (model == EffectLightModel.UNKNOWN) {
logger.debug( logger.debug(
"Could not determine effect light type for thing {}, if you feel this is wrong request adding support on GitHub.", "Could not determine effect light type for thing {}, if you feel this is wrong request adding support on GitHub.",
thing.getUID()); thing.getUID());
} }
ChannelUID effectChannelUID = new ChannelUID(thing.getUID(), CHANNEL_EFFECT); 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) { switch (model) {
case LIDL_MELINARA: case LIDL_MELINARA:
// additional channels if (thing.getChannel(CHANNEL_EFFECT_SPEED) == null) {
createChannel(CHANNEL_EFFECT_SPEED, ChannelKind.STATE); // additional channels
createChannel(thingBuilder, CHANNEL_EFFECT_SPEED, ChannelKind.STATE);
thingEdited = true;
}
List<String> options = List.of("none", "steady", "snow", "rainbow", "snake", "tinkle", "fireworks", List<String> options = List.of("none", "steady", "snow", "rainbow", "snake", "tinkle", "fireworks",
"flag", "waves", "updown", "vintage", "fading", "collide", "strobe", "sparkles", "carnival", "flag", "waves", "updown", "vintage", "fading", "collide", "strobe", "sparkles", "carnival",
@ -366,84 +373,38 @@ public class LightThingHandler extends DeconzBaseThingHandler {
options = List.of("none", "colorloop"); options = List.of("none", "colorloop");
commandDescriptionProvider.setCommandOptions(effectChannelUID, toCommandOptionList(options)); commandDescriptionProvider.setCommandOptions(effectChannelUID, toCommandOptionList(options));
} }
return thingEdited;
} }
private List<CommandOption> toCommandOptionList(List<String> options) { private List<CommandOption> toCommandOptionList(List<String> options) {
return options.stream().map(c -> new CommandOption(c, c)).collect(Collectors.toList()); return options.stream().map(c -> new CommandOption(c, c)).collect(Collectors.toList());
} }
private void valueUpdated(String channelId, LightState newState) { private void valueUpdated(ChannelUID channelUID, LightState newState) {
Integer bri = newState.bri;
Integer hue = newState.hue;
Integer sat = newState.sat;
Boolean on = newState.on; Boolean on = newState.on;
switch (channelId) { switch (channelUID.getId()) {
case CHANNEL_ALERT: case CHANNEL_ALERT -> updateStringChannel(channelUID, newState.alert);
String alert = newState.alert; case CHANNEL_SWITCH, CHANNEL_LOCK -> updateSwitchChannel(channelUID, on);
if (alert != null) { case CHANNEL_COLOR -> updateColorChannel(channelUID, newState);
updateState(channelId, new StringType(alert)); case CHANNEL_BRIGHTNESS -> updatePercentTypeChannel(channelUID, newState.bri, newState.on);
} case CHANNEL_COLOR_TEMPERATURE -> {
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:
Integer ct = newState.ct; Integer ct = newState.ct;
if (ct != null && ct >= ctMin && ct <= ctMax) { if (ct != null && ct >= ctMin && ct <= ctMax) {
updateState(channelId, new DecimalType(miredToKelvin(ct))); updateState(channelUID, new DecimalType(miredToKelvin(ct)));
} }
break; }
case CHANNEL_POSITION: case CHANNEL_POSITION -> updatePercentTypeChannel(channelUID, newState.bri, true); // always post value
if (bri != null) { case CHANNEL_EFFECT -> updateStringChannel(channelUID, newState.effect);
updateState(channelId, toPercentType(bri)); case CHANNEL_EFFECT_SPEED -> updateDecimalTypeChannel(channelUID, newState.effectSpeed);
}
break;
case CHANNEL_EFFECT:
String effect = newState.effect;
if (effect != null) {
updateState(channelId, new StringType(effect));
}
break;
case CHANNEL_EFFECT_SPEED:
Integer effectSpeed = newState.effectSpeed;
if (effectSpeed != null) {
updateState(channelId, new DecimalType(effectSpeed));
}
break;
default:
} }
} }
@Override @Override
public void messageReceived(String sensorID, DeconzBaseMessage message) { public void messageReceived(DeconzBaseMessage message) {
if (message instanceof LightMessage) { logger.trace("{} received {}", thing.getUID(), message);
LightMessage lightMessage = (LightMessage) message; if (message instanceof LightMessage lightMessage) {
logger.trace("{} received {}", thing.getUID(), lightMessage);
LightState lightState = lightMessage.state; LightState lightState = lightMessage.state;
if (lightState != null) { if (lightState != null) {
if (lastCommandExpireTimestamp > System.currentTimeMillis() if (lastCommandExpireTimestamp > System.currentTimeMillis()
@ -462,12 +423,34 @@ public class LightThingHandler extends DeconzBaseThingHandler {
lightStateCache = lightState; lightStateCache = lightState;
if (Boolean.TRUE.equals(lightState.reachable)) { if (Boolean.TRUE.equals(lightState.reachable)) {
updateStatus(ThingStatus.ONLINE); 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 { } else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "@text/offline.light-not-reachable"); updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "@text/offline.sensor-not-reachable");
thing.getChannels().stream().map(c -> c.getUID()).forEach(c -> updateState(c, UnDefType.UNDEF));
} }
} }
} }
} }
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)));
}
}
} }

View File

@ -17,28 +17,20 @@ import static org.openhab.binding.deconz.internal.BindingConstants.*;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects; 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.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.deconz.internal.Util; import org.openhab.binding.deconz.internal.Util;
import org.openhab.binding.deconz.internal.dto.DeconzBaseMessage; import org.openhab.binding.deconz.internal.dto.DeconzBaseMessage;
import org.openhab.binding.deconz.internal.dto.SensorConfig; import org.openhab.binding.deconz.internal.dto.SensorConfig;
import org.openhab.binding.deconz.internal.dto.SensorMessage; import org.openhab.binding.deconz.internal.dto.SensorMessage;
import org.openhab.binding.deconz.internal.dto.SensorState; import org.openhab.binding.deconz.internal.dto.SensorState;
import org.openhab.binding.deconz.internal.types.ResourceType; 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.Channel;
import org.openhab.core.thing.ChannelUID; import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing; import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus; import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail; 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.thing.type.ChannelKind;
import org.openhab.core.types.Command; import org.openhab.core.types.Command;
import org.slf4j.Logger; 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 * Prevent a dispose/init cycle while this flag is set. Use for property updates
*/ */
private boolean ignoreConfigurationUpdate; private boolean ignoreConfigurationUpdate;
private @Nullable ScheduledFuture<?> lastSeenPollingJob;
public SensorBaseThingHandler(Thing thing, Gson gson) { public SensorBaseThingHandler(Thing thing, Gson gson) {
super(thing, gson, ResourceType.SENSORS); 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 @Override
public abstract void handleCommand(ChannelUID channelUID, Command command); 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(); protected abstract List<String> getConfigChannels();
@ -106,11 +87,10 @@ public abstract class SensorBaseThingHandler extends DeconzBaseThingHandler {
@Override @Override
protected void processStateResponse(DeconzBaseMessage stateResponse) { protected void processStateResponse(DeconzBaseMessage stateResponse) {
if (!(stateResponse instanceof SensorMessage)) { if (!(stateResponse instanceof SensorMessage sensorMessage)) {
return; return;
} }
SensorMessage sensorMessage = (SensorMessage) stateResponse;
sensorConfig = Objects.requireNonNullElse(sensorMessage.config, new SensorConfig()); sensorConfig = Objects.requireNonNullElse(sensorMessage.config, new SensorConfig());
sensorState = Objects.requireNonNullElse(sensorMessage.state, new SensorState()); sensorState = Objects.requireNonNullElse(sensorMessage.state, new SensorState());
@ -133,34 +113,38 @@ public abstract class SensorBaseThingHandler extends DeconzBaseThingHandler {
// Some sensors support optional channels // Some sensors support optional channels
// (see https://github.com/dresden-elektronik/deconz-rest-plugin/wiki/Supported-Devices#sensors) // (see https://github.com/dresden-elektronik/deconz-rest-plugin/wiki/Supported-Devices#sensors)
// any battery-powered sensor // any battery-powered sensor
ThingBuilder thingBuilder = editThing();
boolean thingEdited = false;
if (sensorConfig.battery != null) { if (sensorConfig.battery != null) {
createChannel(CHANNEL_BATTERY_LEVEL, ChannelKind.STATE); if (createChannel(thingBuilder, CHANNEL_BATTERY_LEVEL, ChannelKind.STATE)) {
createChannel(CHANNEL_BATTERY_LOW, 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) { if (createTypeSpecificChannels(thingBuilder, sensorConfig, sensorState)) {
createChannel(CHANNEL_BATTERY_LOW, ChannelKind.STATE); thingEdited = true;
} }
createTypeSpecificChannels(sensorConfig, sensorState); if (checkLastSeen(thingBuilder, sensorMessage.lastseen)) {
thingEdited = true;
}
// if the thing was edited, we update it now
if (thingEdited) {
logger.debug("Thing configuration changed, updating thing.");
updateThing(thingBuilder.build());
}
ignoreConfigurationUpdate = false; ignoreConfigurationUpdate = false;
// "Last seen" is the last "ping" from the device, whereas "last update" is the last status changed.
// For example, for a fire sensor, the device pings regularly, without necessarily updating channels.
// So to monitor a sensor is still alive, the "last seen" is necessary.
// Because "last seen" is never updated by the WebSocket API - if this is supported, then we have to
// manually poll it after the defined time
String lastSeen = sensorMessage.lastseen;
if (lastSeen != null && config.lastSeenPolling > 0) {
createChannel(CHANNEL_LAST_SEEN, ChannelKind.STATE);
updateState(CHANNEL_LAST_SEEN, Util.convertTimestampToDateTime(lastSeen));
lastSeenPollingJob = scheduler.schedule(() -> requestState(this::processLastSeen), config.lastSeenPolling,
TimeUnit.MINUTES);
logger.trace("lastSeen polling enabled for thing {} with interval of {} minutes", thing.getUID(),
config.lastSeenPolling);
}
// Initial data // Initial data
updateChannels(sensorConfig); updateChannels(sensorConfig);
updateChannels(sensorState, true); updateChannels(sensorState, true);
@ -168,13 +152,6 @@ public abstract class SensorBaseThingHandler extends DeconzBaseThingHandler {
updateStatus(ThingStatus.ONLINE); 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 * 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) { protected void valueUpdated(ChannelUID channelUID, SensorConfig newConfig) {
Integer batteryLevel = newConfig.battery; Integer batteryLevel = newConfig.battery;
switch (channelUID.getId()) { if (batteryLevel != null) {
case CHANNEL_BATTERY_LEVEL: switch (channelUID.getId()) {
if (batteryLevel != null) { case CHANNEL_BATTERY_LEVEL -> updateDecimalTypeChannel(channelUID, batteryLevel.longValue());
updateState(channelUID, new DecimalType(batteryLevel.longValue())); case CHANNEL_BATTERY_LOW -> updateSwitchChannel(channelUID, batteryLevel <= 10);
} // other cases covered by subclass
break; }
case CHANNEL_BATTERY_LOW:
if (batteryLevel != null) {
updateState(channelUID, OnOffType.from(batteryLevel <= 10));
}
break;
default:
// other cases covered by sub-class
} }
} }
@ -208,32 +178,29 @@ public abstract class SensorBaseThingHandler extends DeconzBaseThingHandler {
*/ */
protected void valueUpdated(ChannelUID channelUID, SensorState newState, boolean initializing) { protected void valueUpdated(ChannelUID channelUID, SensorState newState, boolean initializing) {
switch (channelUID.getId()) { switch (channelUID.getId()) {
case CHANNEL_LAST_UPDATED: case CHANNEL_LAST_UPDATED -> {
String lastUpdated = newState.lastupdated; String lastUpdated = newState.lastupdated;
if (lastUpdated != null && !"none".equals(lastUpdated)) { if (lastUpdated != null && !"none".equals(lastUpdated)) {
updateState(channelUID, Util.convertTimestampToDateTime(lastUpdated)); updateState(channelUID, Util.convertTimestampToDateTime(lastUpdated));
} }
break; }
case CHANNEL_BATTERY_LOW: case CHANNEL_BATTERY_LOW -> updateSwitchChannel(channelUID, newState.lowbattery);
Boolean lowBattery = newState.lowbattery; // other cases covered by subclass
if (lowBattery != null) {
updateState(channelUID, OnOffType.from(lowBattery));
}
break;
default:
// other cases covered by sub-class
} }
} }
@Override @Override
public void messageReceived(String sensorID, DeconzBaseMessage message) { public void messageReceived(DeconzBaseMessage message) {
logger.trace("{} received {}", thing.getUID(), message); logger.trace("{} received {}", thing.getUID(), message);
if (message instanceof SensorMessage) { if (message instanceof SensorMessage sensorMessage) {
SensorMessage sensorMessage = (SensorMessage) message;
SensorConfig sensorConfig = sensorMessage.config; SensorConfig sensorConfig = sensorMessage.config;
if (sensorConfig != null) { if (sensorConfig != null) {
this.sensorConfig = sensorConfig; if (sensorConfig.reachable) {
updateChannels(sensorConfig); updateStatus(ThingStatus.ONLINE);
updateChannels(sensorConfig);
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "@text/offline.sensor-not-reachable");
}
} }
SensorState sensorState = sensorMessage.state; SensorState sensorState = sensorMessage.state;
if (sensorState != null) { if (sensorState != null) {
@ -243,6 +210,7 @@ public abstract class SensorBaseThingHandler extends DeconzBaseThingHandler {
} }
private void updateChannels(SensorConfig newConfig) { private void updateChannels(SensorConfig newConfig) {
this.sensorConfig = newConfig;
List<String> configChannels = getConfigChannels(); List<String> configChannels = getConfigChannels();
thing.getChannels().stream().map(Channel::getUID) thing.getChannels().stream().map(Channel::getUID)
.filter(channelUID -> configChannels.contains(channelUID.getId())) .filter(channelUID -> configChannels.contains(channelUID.getId()))
@ -253,34 +221,4 @@ public abstract class SensorBaseThingHandler extends DeconzBaseThingHandler {
sensorState = newState; sensorState = newState;
thing.getChannels().forEach(channel -> valueUpdated(channel.getUID(), newState, initializing)); 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));
}
} }

View 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.dto.ThermostatUpdateConfig;
import org.openhab.binding.deconz.internal.types.ThermostatMode; import org.openhab.binding.deconz.internal.types.ThermostatMode;
import org.openhab.core.library.types.DecimalType; 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.OpenClosedType;
import org.openhab.core.library.types.QuantityType; import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.types.StringType; import org.openhab.core.library.types.StringType;
import org.openhab.core.thing.ChannelUID; import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing; import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID; 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.thing.type.ChannelKind;
import org.openhab.core.types.Command; import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType; import org.openhab.core.types.RefreshType;
import org.openhab.core.types.UnDefType;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; 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); 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, 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); private final Logger logger = LoggerFactory.getLogger(SensorThermostatThingHandler.class);
@ -83,23 +86,24 @@ public class SensorThermostatThingHandler extends SensorBaseThingHandler {
} }
ThermostatUpdateConfig newConfig = new ThermostatUpdateConfig(); ThermostatUpdateConfig newConfig = new ThermostatUpdateConfig();
switch (channelUID.getId()) { switch (channelUID.getId()) {
case CHANNEL_HEATSETPOINT: case CHANNEL_THERMOSTAT_LOCKED -> newConfig.locked = OnOffType.ON.equals(command);
case CHANNEL_HEATSETPOINT -> {
Integer newHeatsetpoint = getTemperatureFromCommand(command); Integer newHeatsetpoint = getTemperatureFromCommand(command);
if (newHeatsetpoint == null) { if (newHeatsetpoint == null) {
logger.warn("Heatsetpoint must not be null."); logger.warn("Heatsetpoint must not be null.");
return; return;
} }
newConfig.heatsetpoint = newHeatsetpoint; newConfig.heatsetpoint = newHeatsetpoint;
break; }
case CHANNEL_TEMPERATURE_OFFSET: case CHANNEL_TEMPERATURE_OFFSET -> {
Integer newOffset = getTemperatureFromCommand(command); Integer newOffset = getTemperatureFromCommand(command);
if (newOffset == null) { if (newOffset == null) {
logger.warn("Offset must not be null."); logger.warn("Offset must not be null.");
return; return;
} }
newConfig.offset = newOffset; newConfig.offset = newOffset;
break; }
case CHANNEL_THERMOSTAT_MODE: case CHANNEL_THERMOSTAT_MODE -> {
if (command instanceof StringType) { if (command instanceof StringType) {
String thermostatMode = ((StringType) command).toString(); String thermostatMode = ((StringType) command).toString();
try { try {
@ -117,11 +121,12 @@ public class SensorThermostatThingHandler extends SensorBaseThingHandler {
} else { } else {
return; return;
} }
break; }
default: case CHANNEL_EXTERNAL_WINDOW_OPEN -> newConfig.externalwindowopen = OpenClosedType.OPEN.equals(command);
default -> {
// no supported command // no supported command
return; return;
}
} }
sendCommand(newConfig, command, channelUID, null); sendCommand(newConfig, command, channelUID, null);
@ -133,15 +138,18 @@ public class SensorThermostatThingHandler extends SensorBaseThingHandler {
ThermostatMode thermostatMode = newConfig.mode; ThermostatMode thermostatMode = newConfig.mode;
String mode = thermostatMode != null ? thermostatMode.name() : ThermostatMode.UNKNOWN.name(); String mode = thermostatMode != null ? thermostatMode.name() : ThermostatMode.UNKNOWN.name();
switch (channelUID.getId()) { switch (channelUID.getId()) {
case CHANNEL_HEATSETPOINT: case CHANNEL_THERMOSTAT_LOCKED -> updateSwitchChannel(channelUID, newConfig.locked);
updateQuantityTypeChannel(channelUID, newConfig.heatsetpoint, CELSIUS, 1.0 / 100); case CHANNEL_HEATSETPOINT -> updateQuantityTypeChannel(channelUID, newConfig.heatsetpoint, CELSIUS,
break; 1.0 / 100);
case CHANNEL_TEMPERATURE_OFFSET: case CHANNEL_TEMPERATURE_OFFSET -> updateQuantityTypeChannel(channelUID, newConfig.offset, CELSIUS,
updateQuantityTypeChannel(channelUID, newConfig.offset, CELSIUS, 1.0 / 100); 1.0 / 100);
break; case CHANNEL_THERMOSTAT_MODE -> updateState(channelUID, new StringType(mode));
case CHANNEL_THERMOSTAT_MODE: case CHANNEL_EXTERNAL_WINDOW_OPEN -> {
updateState(channelUID, new StringType(mode)); Boolean open = newConfig.externalwindowopen;
break; 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) { protected void valueUpdated(ChannelUID channelUID, SensorState newState, boolean initializing) {
super.valueUpdated(channelUID, newState, initializing); super.valueUpdated(channelUID, newState, initializing);
switch (channelUID.getId()) { switch (channelUID.getId()) {
case CHANNEL_TEMPERATURE: case CHANNEL_TEMPERATURE -> updateQuantityTypeChannel(channelUID, newState.temperature, CELSIUS, 1.0 / 100);
updateQuantityTypeChannel(channelUID, newState.temperature, CELSIUS, 1.0 / 100); case CHANNEL_VALVE_POSITION -> {
break; Integer valve = newState.valve;
case CHANNEL_VALVE_POSITION: if (valve == null || valve < 0 || valve > 100) {
updateQuantityTypeChannel(channelUID, newState.valve, PERCENT, 100.0 / 255); updateState(channelUID, UnDefType.UNDEF);
break; } else {
case CHANNEL_WINDOWOPEN: updateQuantityTypeChannel(channelUID, valve, PERCENT, 1.0);
}
}
case CHANNEL_WINDOW_OPEN -> {
String open = newState.windowopen; String open = newState.windowopen;
if (open != null) { if (open != null) {
updateState(channelUID, "Closed".equals(open) ? OpenClosedType.CLOSED : OpenClosedType.OPEN); updateState(channelUID, "Closed".equals(open) ? OpenClosedType.CLOSED : OpenClosedType.OPEN);
} }
break; }
} }
} }
@Override @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 @Override
@ -193,14 +210,30 @@ public class SensorThermostatThingHandler extends SensorBaseThingHandler {
@Override @Override
protected void processStateResponse(DeconzBaseMessage stateResponse) { protected void processStateResponse(DeconzBaseMessage stateResponse) {
if (!(stateResponse instanceof SensorMessage)) { if (!(stateResponse instanceof SensorMessage sensorMessage)) {
return; return;
} }
SensorMessage sensorMessage = (SensorMessage) stateResponse;
SensorState sensorState = sensorMessage.state; SensorState sensorState = sensorMessage.state;
SensorConfig sensorConfig = sensorMessage.config;
boolean changed = false;
ThingBuilder thingBuilder = editThing();
if (sensorState != null && sensorState.windowopen != null) { 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); super.processStateResponse(stateResponse);

View 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.SensorConfig;
import org.openhab.binding.deconz.internal.dto.SensorState; import org.openhab.binding.deconz.internal.dto.SensorState;
import org.openhab.binding.deconz.internal.dto.SensorUpdateConfig; 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.OnOffType;
import org.openhab.core.library.types.OpenClosedType; import org.openhab.core.library.types.OpenClosedType;
import org.openhab.core.library.types.QuantityType; 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.ChannelUID;
import org.openhab.core.thing.Thing; import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID; 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.thing.type.ChannelKind;
import org.openhab.core.types.Command; import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType; import org.openhab.core.types.RefreshType;
import org.openhab.core.util.ColorUtil;
import com.google.gson.Gson; 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_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_OPENCLOSE_SENSOR, THING_TYPE_WATERLEAKAGE_SENSOR, THING_TYPE_FIRE_SENSOR,
THING_TYPE_ALARM_SENSOR, THING_TYPE_VIBRATION_SENSOR, THING_TYPE_BATTERY_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, private static final List<String> CONFIG_CHANNELS = List.of(CHANNEL_BATTERY_LEVEL, CHANNEL_BATTERY_LOW,
CHANNEL_ENABLED, CHANNEL_TEMPERATURE); CHANNEL_ENABLED, CHANNEL_TEMPERATURE);
@ -91,15 +93,13 @@ public class SensorThingHandler extends SensorBaseThingHandler {
protected void valueUpdated(ChannelUID channelUID, SensorConfig newConfig) { protected void valueUpdated(ChannelUID channelUID, SensorConfig newConfig) {
super.valueUpdated(channelUID, newConfig); super.valueUpdated(channelUID, newConfig);
switch (channelUID.getId()) { switch (channelUID.getId()) {
case CHANNEL_ENABLED: case CHANNEL_ENABLED -> updateState(channelUID, OnOffType.from(newConfig.on));
updateState(channelUID, OnOffType.from(newConfig.on)); case CHANNEL_TEMPERATURE -> {
break;
case CHANNEL_TEMPERATURE:
Float temperature = newConfig.temperature; Float temperature = newConfig.temperature;
if (temperature != null) { if (temperature != null) {
updateState(channelUID, new QuantityType<>(temperature / 100, CELSIUS)); 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) { protected void valueUpdated(ChannelUID channelUID, SensorState newState, boolean initializing) {
super.valueUpdated(channelUID, newState, initializing); super.valueUpdated(channelUID, newState, initializing);
switch (channelUID.getId()) { switch (channelUID.getId()) {
case CHANNEL_BATTERY_LEVEL: case CHANNEL_BATTERY_LEVEL -> updateDecimalTypeChannel(channelUID, newState.battery);
updateDecimalTypeChannel(channelUID, newState.battery); case CHANNEL_LIGHT -> {
break;
case CHANNEL_LIGHT:
Boolean dark = newState.dark; Boolean dark = newState.dark;
if (dark != null) { if (dark != null) {
Boolean daylight = newState.daylight; Boolean daylight = newState.daylight;
@ -126,138 +124,103 @@ public class SensorThingHandler extends SensorBaseThingHandler {
updateState(channelUID, new StringType("Daylight")); updateState(channelUID, new StringType("Daylight"));
} }
} }
break; }
case CHANNEL_POWER: case CHANNEL_POWER -> updateQuantityTypeChannel(channelUID, newState.power, WATT);
updateQuantityTypeChannel(channelUID, newState.power, WATT); case CHANNEL_CONSUMPTION -> updateQuantityTypeChannel(channelUID, newState.consumption, WATT_HOUR);
break; case CHANNEL_VOLTAGE -> updateQuantityTypeChannel(channelUID, newState.voltage, VOLT);
case CHANNEL_CONSUMPTION: case CHANNEL_CURRENT -> updateQuantityTypeChannel(channelUID, newState.current, MILLI(AMPERE));
updateQuantityTypeChannel(channelUID, newState.consumption, WATT_HOUR); case CHANNEL_LIGHT_LUX -> updateQuantityTypeChannel(channelUID, newState.lux, LUX);
break; case CHANNEL_COLOR -> {
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:
final double @Nullable [] xy = newState.xy; final double @Nullable [] xy = newState.xy;
if (xy != null && xy.length == 2) { 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: case CHANNEL_LIGHT_LEVEL -> updateDecimalTypeChannel(channelUID, newState.lightlevel);
updateDecimalTypeChannel(channelUID, newState.lightlevel); case CHANNEL_DARK -> updateSwitchChannel(channelUID, newState.dark);
break; case CHANNEL_DAYLIGHT -> updateSwitchChannel(channelUID, newState.daylight);
case CHANNEL_DARK: case CHANNEL_TEMPERATURE -> updateQuantityTypeChannel(channelUID, newState.temperature, CELSIUS, 1.0 / 100);
updateSwitchChannel(channelUID, newState.dark); case CHANNEL_HUMIDITY -> updateQuantityTypeChannel(channelUID, newState.humidity, PERCENT, 1.0 / 100);
break; case CHANNEL_PRESSURE -> updateQuantityTypeChannel(channelUID, newState.pressure, HECTO(PASCAL));
case CHANNEL_DAYLIGHT: case CHANNEL_PRESENCE -> updateSwitchChannel(channelUID, newState.presence);
updateSwitchChannel(channelUID, newState.daylight); case CHANNEL_VALUE -> updateDecimalTypeChannel(channelUID, newState.status);
break; case CHANNEL_OPENCLOSE -> {
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:
Boolean open = newState.open; Boolean open = newState.open;
if (open != null) { if (open != null) {
updateState(channelUID, open ? OpenClosedType.OPEN : OpenClosedType.CLOSED); updateState(channelUID, open ? OpenClosedType.OPEN : OpenClosedType.CLOSED);
} }
break; }
case CHANNEL_WATERLEAKAGE: case CHANNEL_WATERLEAKAGE -> updateSwitchChannel(channelUID, newState.water);
updateSwitchChannel(channelUID, newState.water); case CHANNEL_FIRE -> updateSwitchChannel(channelUID, newState.fire);
break; case CHANNEL_ALARM -> updateSwitchChannel(channelUID, newState.alarm);
case CHANNEL_FIRE: case CHANNEL_TAMPERED -> updateSwitchChannel(channelUID, newState.tampered);
updateSwitchChannel(channelUID, newState.fire); case CHANNEL_VIBRATION -> updateSwitchChannel(channelUID, newState.vibration);
break; case CHANNEL_CARBONMONOXIDE -> updateSwitchChannel(channelUID, newState.carbonmonoxide);
case CHANNEL_ALARM: case CHANNEL_AIRQUALITY -> updateStringChannel(channelUID, newState.airquality);
updateSwitchChannel(channelUID, newState.alarm); case CHANNEL_AIRQUALITYPPB -> updateQuantityTypeChannel(channelUID, newState.airqualityppb,
break; PARTS_PER_BILLION);
case CHANNEL_TAMPERED: case CHANNEL_MOISTURE -> updateQuantityTypeChannel(channelUID, newState.moisture, PERCENT);
updateSwitchChannel(channelUID, newState.tampered); case CHANNEL_BUTTON -> updateDecimalTypeChannel(channelUID, newState.buttonevent);
break; case CHANNEL_BUTTONEVENT -> {
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:
Integer buttonevent = newState.buttonevent; Integer buttonevent = newState.buttonevent;
if (buttonevent != null && !initializing) { if (buttonevent != null && !initializing) {
triggerChannel(channelUID, String.valueOf(buttonevent)); triggerChannel(channelUID, String.valueOf(buttonevent));
} }
break; }
case CHANNEL_GESTURE: case CHANNEL_GESTURE -> updateDecimalTypeChannel(channelUID, newState.gesture);
updateDecimalTypeChannel(channelUID, newState.gesture); case CHANNEL_GESTUREEVENT -> {
break;
case CHANNEL_GESTUREEVENT:
Integer gesture = newState.gesture; Integer gesture = newState.gesture;
if (gesture != null && !initializing) { if (gesture != null && !initializing) {
triggerChannel(channelUID, String.valueOf(gesture)); triggerChannel(channelUID, String.valueOf(gesture));
} }
break; }
} }
} }
@Override @Override
protected void createTypeSpecificChannels(SensorConfig sensorConfig, SensorState sensorState) { protected boolean createTypeSpecificChannels(ThingBuilder thingBuilder, SensorConfig sensorConfig,
SensorState sensorState) {
boolean thingEdited = false;
// some Xiaomi sensors // some Xiaomi sensors
if (sensorConfig.temperature != null) { if (sensorConfig.temperature != null && createChannel(thingBuilder, CHANNEL_TEMPERATURE, ChannelKind.STATE)) {
createChannel(CHANNEL_TEMPERATURE, ChannelKind.STATE); thingEdited = true;
} }
// ZHAPresence - e.g. IKEA TRÅDFRI motion sensor // ZHAPresence - e.g. IKEA TRÅDFRI motion sensor
if (sensorState.dark != null) { if (sensorState.dark != null && createChannel(thingBuilder, CHANNEL_DARK, ChannelKind.STATE)) {
createChannel(CHANNEL_DARK, ChannelKind.STATE); thingEdited = true;
} }
// ZHAConsumption - e.g Bitron 902010/25 or Heiman SmartPlug // ZHAConsumption - e.g Bitron 902010/25 or Heiman SmartPlug
if (sensorState.power != null) { if (sensorState.power != null && createChannel(thingBuilder, CHANNEL_POWER, ChannelKind.STATE)) {
createChannel(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 // ZHAPower - e.g. Heiman SmartPlug
if (sensorState.voltage != null) { if (sensorState.voltage != null && createChannel(thingBuilder, CHANNEL_VOLTAGE, ChannelKind.STATE)) {
createChannel(CHANNEL_VOLTAGE, ChannelKind.STATE); thingEdited = true;
} }
if (sensorState.current != null) { if (sensorState.current != null && createChannel(thingBuilder, CHANNEL_CURRENT, ChannelKind.STATE)) {
createChannel(CHANNEL_CURRENT, ChannelKind.STATE); thingEdited = true;
} }
// IAS Zone sensor - e.g. Heiman HS1MS motion sensor // IAS Zone sensor - e.g. Heiman HS1MS motion sensor
if (sensorState.tampered != null) { if (sensorState.tampered != null && createChannel(thingBuilder, CHANNEL_TAMPERED, ChannelKind.STATE)) {
createChannel(CHANNEL_TAMPERED, ChannelKind.STATE); thingEdited = true;
} }
// e.g. Aqara Cube // e.g. Aqara Cube
if (sensorState.gesture != null) { if (sensorState.gesture != null && (createChannel(thingBuilder, CHANNEL_GESTURE, ChannelKind.STATE)
createChannel(CHANNEL_GESTURE, ChannelKind.STATE); || createChannel(thingBuilder, CHANNEL_GESTUREEVENT, ChannelKind.TRIGGER))) {
createChannel(CHANNEL_GESTUREEVENT, ChannelKind.TRIGGER); thingEdited = true;
} }
return thingEdited;
} }
@Override @Override

View File

@ -48,7 +48,7 @@ public class AsyncHttpClient {
* @param timeout A timeout * @param timeout A timeout
* @return The result * @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); return doNetwork(HttpMethod.POST, address, jsonString, timeout);
} }
@ -101,15 +101,16 @@ public class AsyncHttpClient {
} }
request.method(method).timeout(timeout, TimeUnit.MILLISECONDS).send(new BufferingResponseListener() { request.method(method).timeout(timeout, TimeUnit.MILLISECONDS).send(new BufferingResponseListener() {
@NonNullByDefault({})
@Override @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(); final HttpResponse response = (HttpResponse) result.getResponse();
if (result.getFailure() != null) { if (result.getFailure() != null) {
f.completeExceptionally(result.getFailure()); f.completeExceptionally(result.getFailure());
return; return;
} }
f.complete(new Result(getContentAsString(), response.getStatus())); String content = getContentAsString();
f.complete(new Result(content != null ? content : "", response.getStatus()));
} }
}); });
return f; return f;

View File

@ -16,6 +16,9 @@ import java.net.URI;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap; 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 java.util.concurrent.atomic.AtomicInteger;
import org.eclipse.jdt.annotation.NonNullByDefault; 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.eclipse.jetty.websocket.client.WebSocketClient;
import org.openhab.binding.deconz.internal.dto.DeconzBaseMessage; import org.openhab.binding.deconz.internal.dto.DeconzBaseMessage;
import org.openhab.binding.deconz.internal.types.ResourceType; import org.openhab.binding.deconz.internal.types.ResourceType;
import org.openhab.core.common.ThreadPoolManager;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -46,23 +50,33 @@ import com.google.gson.Gson;
public class WebSocketConnection { public class WebSocketConnection {
private static final AtomicInteger INSTANCE_COUNTER = new AtomicInteger(); private static final AtomicInteger INSTANCE_COUNTER = new AtomicInteger();
private final Logger logger = LoggerFactory.getLogger(WebSocketConnection.class); private final Logger logger = LoggerFactory.getLogger(WebSocketConnection.class);
private final ScheduledExecutorService scheduler = ThreadPoolManager.getScheduledPool("thingHandler");
private final WebSocketClient client; private final WebSocketClient client;
private final String socketName; private final String socketName;
private final Gson gson; private final Gson gson;
private int watchdogInterval;
private final WebSocketConnectionListener connectionListener; private final WebSocketConnectionListener connectionListener;
private final Map<String, WebSocketMessageListener> listeners = new ConcurrentHashMap<>(); private final Map<String, WebSocketMessageListener> listeners = new ConcurrentHashMap<>();
private ConnectionState connectionState = ConnectionState.DISCONNECTED; private ConnectionState connectionState = ConnectionState.DISCONNECTED;
private @Nullable ScheduledFuture<?> watchdogJob;
private @Nullable Session session; 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.connectionListener = listener;
this.client = client; this.client = client;
this.client.setMaxIdleTimeout(0); this.client.setMaxIdleTimeout(0);
this.gson = gson; this.gson = gson;
this.socketName = "Websocket$" + System.currentTimeMillis() + "-" + INSTANCE_COUNTER.incrementAndGet(); this.socketName = "Websocket$" + System.currentTimeMillis() + "-" + INSTANCE_COUNTER.incrementAndGet();
this.watchdogInterval = watchdogInterval;
}
public void setWatchdogInterval(int watchdogInterval) {
this.watchdogInterval = watchdogInterval;
} }
public void start(String ip) { public void start(String ip) {
@ -73,18 +87,47 @@ public class WebSocketConnection {
return; return;
} else if (connectionState == ConnectionState.DISCONNECTING) { } else if (connectionState == ConnectionState.DISCONNECTING) {
logger.warn("{} trying to re-connect while still disconnecting", socketName); logger.warn("{} trying to re-connect while still disconnecting", socketName);
return;
} }
try { try {
connectionState = ConnectionState.CONNECTING;
URI destUri = URI.create("ws://" + ip); URI destUri = URI.create("ws://" + ip);
client.start(); client.start();
logger.debug("Trying to connect {} to {}", socketName, destUri); logger.debug("Trying to connect {} to {}", socketName, destUri);
client.connect(this, destUri).get(); client.connect(this, destUri).get();
} catch (Exception e) { } catch (Exception e) {
connectionListener.connectionLost("Error while connecting: " + e.getMessage()); String reason = "Error while connecting: " + e.getMessage();
if (e.getMessage() == null) {
logger.warn("{}: {}", socketName, reason, e);
} else {
logger.warn("{}: {}", socketName, reason);
}
connectionListener.webSocketConnectionLost(reason);
} }
} }
public void close() { private void startOrResetWatchdogTimer() {
stopWatchdogTimer(); // stop already running timer
watchdogJob = scheduler.schedule(
() -> connectionListener.webSocketConnectionLost(
"Watchdog timed out after " + watchdogInterval + "s. Websocket seems to be dead."),
watchdogInterval, TimeUnit.SECONDS);
}
private void stopWatchdogTimer() {
ScheduledFuture<?> watchdogTimer = this.watchdogJob;
if (watchdogTimer != null) {
watchdogTimer.cancel(false);
this.watchdogJob = null;
}
}
/**
* dispose the websocket (close connection and destroy client)
*
*/
public void dispose() {
stopWatchdogTimer();
try { try {
connectionState = ConnectionState.DISCONNECTING; connectionState = ConnectionState.DISCONNECTING;
client.stop(); client.stop();
@ -92,6 +135,7 @@ public class WebSocketConnection {
logger.debug("{} encountered an error while closing connection", socketName, e); logger.debug("{} encountered an error while closing connection", socketName, e);
} }
client.destroy(); client.destroy();
connectionState = ConnectionState.DISCONNECTED;
} }
public void registerListener(ResourceType resourceType, String sensorID, WebSocketMessageListener listener) { public void registerListener(ResourceType resourceType, String sensorID, WebSocketMessageListener listener) {
@ -108,17 +152,19 @@ public class WebSocketConnection {
connectionState = ConnectionState.CONNECTED; connectionState = ConnectionState.CONNECTED;
logger.debug("{} successfully connected to {}: {}", socketName, session.getRemoteAddress().getAddress(), logger.debug("{} successfully connected to {}: {}", socketName, session.getRemoteAddress().getAddress(),
session.hashCode()); session.hashCode());
connectionListener.connectionEstablished(); connectionListener.webSocketConnectionEstablished();
startOrResetWatchdogTimer();
this.session = session; this.session = session;
} }
@SuppressWarnings({ "null", "unused" }) @SuppressWarnings("unused")
@OnWebSocketMessage @OnWebSocketMessage
public void onMessage(Session session, String message) { public void onMessage(Session session, String message) {
if (!session.equals(this.session)) { if (!session.equals(this.session)) {
handleWrongSession(session, message); handleWrongSession(session, message);
return; return;
} }
startOrResetWatchdogTimer();
logger.trace("{} received raw data: {}", socketName, message); logger.trace("{} received raw data: {}", socketName, message);
try { try {
@ -128,7 +174,16 @@ public class WebSocketConnection {
return; 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) { if (listener == null) {
logger.trace( 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.", "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; return;
} }
// we still need the original resource type here
Class<? extends DeconzBaseMessage> expectedMessageType = changedMessage.r.getExpectedMessageType(); Class<? extends DeconzBaseMessage> expectedMessageType = changedMessage.r.getExpectedMessageType();
if (expectedMessageType == null) { if (expectedMessageType == null) {
logger.warn( logger.warn(
@ -144,11 +200,8 @@ public class WebSocketConnection {
return; return;
} }
DeconzBaseMessage deconzMessage = gson.fromJson(message, expectedMessageType); DeconzBaseMessage deconzMessage = Objects.requireNonNull(gson.fromJson(message, expectedMessageType));
if (deconzMessage != null) { listener.messageReceived(deconzMessage);
listener.messageReceived(changedMessage.id, deconzMessage);
}
} catch (RuntimeException e) { } catch (RuntimeException e) {
// we need to catch all processing exceptions, otherwise they could affect the connection // we need to catch all processing exceptions, otherwise they could affect the connection
logger.warn("{} encountered an error while processing the message {}: {}", socketName, message, logger.warn("{} encountered an error while processing the message {}: {}", socketName, message,
@ -159,17 +212,13 @@ public class WebSocketConnection {
@SuppressWarnings("unused") @SuppressWarnings("unused")
@OnWebSocketError @OnWebSocketError
public void onError(@Nullable Session session, Throwable cause) { public void onError(@Nullable Session session, Throwable cause) {
if (session == null) { if (session != null && !session.equals(this.session)) {
logger.trace("Encountered an error while processing on error without session. Connection state is {}: {}",
connectionState, cause.getMessage());
return;
}
if (!session.equals(this.session)) {
handleWrongSession(session, "Connection error: " + cause.getMessage()); handleWrongSession(session, "Connection error: " + cause.getMessage());
return; return;
} }
logger.warn("{} connection errored, closing: {}", socketName, cause.getMessage()); logger.warn("{} connection errored, closing: {}", socketName, cause.getMessage());
stopWatchdogTimer();
Session storedSession = this.session; Session storedSession = this.session;
if (storedSession != null && storedSession.isOpen()) { if (storedSession != null && storedSession.isOpen()) {
storedSession.close(-1, "Processing error"); storedSession.close(-1, "Processing error");
@ -185,12 +234,13 @@ public class WebSocketConnection {
} }
logger.trace("{} closed connection: {} / {}", socketName, statusCode, reason); logger.trace("{} closed connection: {} / {}", socketName, statusCode, reason);
connectionState = ConnectionState.DISCONNECTED; connectionState = ConnectionState.DISCONNECTED;
stopWatchdogTimer();
this.session = null; this.session = null;
connectionListener.connectionLost(reason); connectionListener.webSocketConnectionLost(reason);
} }
private void handleWrongSession(Session session, String message) { 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); session.hashCode(), message);
if (session.isOpen()) { if (session.isOpen()) {
// Close the session if it is still open. It should already be closed anyway // Close the session if it is still open. It should already be closed anyway

View File

@ -24,12 +24,12 @@ public interface WebSocketConnectionListener {
/** /**
* Connection successfully established. * Connection successfully established.
*/ */
void connectionEstablished(); void webSocketConnectionEstablished();
/** /**
* Connection lost. A reconnect timer has been started. * Connection lost. A reconnect timer has been started.
* *
* @param reason A reason for the disconnection * @param reason A reason for the disconnection
*/ */
void connectionLost(String reason); void webSocketConnectionLost(String reason);
} }

View File

@ -25,8 +25,7 @@ public interface WebSocketMessageListener {
/** /**
* A new message was received * A new message was received
* *
* @param sensorID The sensor ID (API endpoint)
* @param message The received message * @param message The received message
*/ */
void messageReceived(String sensorID, DeconzBaseMessage message); void messageReceived(DeconzBaseMessage message);
} }

View File

@ -21,20 +21,24 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory; 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 * @author Jan N. Klug - Initial contribution
*/ */
@NonNullByDefault @NonNullByDefault
public enum GroupType { public enum GroupType {
LIGHT_GROUP("LightGroup"), LIGHT_GROUP("LightGroup"),
LUMINAIRE("Luminaire"),
ROOM("Room"),
LIGHT_SOURCE("Lightsource"),
UNKNOWN(""); UNKNOWN("");
private static final Map<String, GroupType> MAPPING = Arrays.stream(GroupType.values()) private static final Map<String, GroupType> MAPPING = Arrays.stream(GroupType.values())
.collect(Collectors.toMap(v -> v.type, v -> v)); .collect(Collectors.toMap(v -> v.type, v -> v));
private static final Logger LOGGER = LoggerFactory.getLogger(GroupType.class); private static final Logger LOGGER = LoggerFactory.getLogger(GroupType.class);
private String type; private final String type;
GroupType(String type) { GroupType(String type) {
this.type = type; this.type = type;

View File

@ -21,7 +21,8 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory; 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 * @author Jan N. Klug - Initial contribution
*/ */
@ -46,7 +47,7 @@ public enum LightType {
.collect(Collectors.toMap(v -> v.type, v -> v)); .collect(Collectors.toMap(v -> v.type, v -> v));
private static final Logger LOGGER = LoggerFactory.getLogger(LightType.class); private static final Logger LOGGER = LoggerFactory.getLogger(LightType.class);
private String type; private final String type;
LightType(String type) { LightType(String type) {
this.type = type; this.type = type;

View File

@ -35,14 +35,15 @@ public enum ResourceType {
GROUPS("groups", "action", GroupMessage.class), GROUPS("groups", "action", GroupMessage.class),
LIGHTS("lights", "state", LightMessage.class), LIGHTS("lights", "state", LightMessage.class),
SENSORS("sensors", "config", SensorMessage.class), SENSORS("sensors", "config", SensorMessage.class),
SCENES("scenes", "", DeconzBaseMessage.class),
UNKNOWN("", "", null); UNKNOWN("", "", null);
private static final Map<String, ResourceType> MAPPING = Arrays.stream(ResourceType.values()) private static final Map<String, ResourceType> MAPPING = Arrays.stream(ResourceType.values())
.collect(Collectors.toMap(v -> v.identifier, v -> v)); .collect(Collectors.toMap(v -> v.identifier, v -> v));
private static final Logger LOGGER = LoggerFactory.getLogger(ResourceType.class); private static final Logger LOGGER = LoggerFactory.getLogger(ResourceType.class);
private String identifier; private final String identifier;
private String commandUrl; private final String commandUrl;
private @Nullable Class<? extends DeconzBaseMessage> expectedMessageType; private @Nullable Class<? extends DeconzBaseMessage> expectedMessageType;
ResourceType(String identifier, String commandUrl, ResourceType(String identifier, String commandUrl,

View File

@ -21,7 +21,8 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory; 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 * @author Lukas Agethen - Initial contribution
*/ */
@ -36,7 +37,7 @@ public enum ThermostatMode {
.collect(Collectors.toMap(v -> v.deconzValue, v -> v)); .collect(Collectors.toMap(v -> v.deconzValue, v -> v));
private static final Logger LOGGER = LoggerFactory.getLogger(ThermostatMode.class); private static final Logger LOGGER = LoggerFactory.getLogger(ThermostatMode.class);
private String deconzValue; private final String deconzValue;
ThermostatMode(String deconzValue) { ThermostatMode(String deconzValue) {
this.deconzValue = deconzValue; this.deconzValue = deconzValue;

View 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"> 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"> <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"> <parameter name="host" type="text" required="true">
<label>Host Address</label> <label>Host Address</label>
<context>network-address</context> <context>network-address</context>
<description>IP address or host name of deCONZ interface.</description> <description>IP address or host name of deCONZ interface.</description>
</parameter> </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"> <parameter name="apikey" type="text">
<label>API Key</label> <label>API Key</label>
<context>password</context> <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 <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> web interface.</description>
</parameter> </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> <label>Timeout</label>
<description>Timeout for asynchronous HTTP requests (in milliseconds).</description> <description>Timeout for asynchronous HTTP requests (in milliseconds).</description>
<advanced>true</advanced> <advanced>true</advanced>
<default>2000</default> <default>2000</default>
</parameter> </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>
<config-description uri="thing-type:deconz:sensor"> <config-description uri="thing-type:deconz:sensor">
@ -56,6 +91,12 @@
<label>Transition Time</label> <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> <description>Time to move between two states. If empty, the default of the device is used. Resolution is 1/10 second.</description>
</parameter> </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>
<config-description uri="thing-type:deconz:colorlight"> <config-description uri="thing-type:deconz:colorlight">
@ -76,21 +117,12 @@
</options> </options>
<advanced>true</advanced> <advanced>true</advanced>
</parameter> </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>
<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>
</config-description>
</config-description:config-descriptions> </config-description:config-descriptions>

View File

@ -5,59 +5,42 @@ addon.deconz.description = Allows to use the real-time channel of the deCONZ sof
# thing types # thing types
thing-type.deconz.airqualitysensor.label = Carbon-monoxide Sensor
thing-type.deconz.alarmsensor.label = Alarm 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.label = Battery Sensor
thing-type.deconz.batterysensor.description = A battery sensor
thing-type.deconz.carbonmonoxidesensor.label = Carbon-monoxide 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.colorcontrol.label = Color Controller
thing-type.deconz.colorlight.label = Color Light thing-type.deconz.colorlight.label = Color Light
thing-type.deconz.colorlight.description = A dimmable light with adjustable color. thing-type.deconz.colorlight.description = A dimmable light with adjustable color.
thing-type.deconz.colortemperaturelight.label = Color-Temperature Light thing-type.deconz.colortemperaturelight.label = Color-Temperature Light
thing-type.deconz.colortemperaturelight.description = A dimmable light with adjustable color temperature. thing-type.deconz.colortemperaturelight.description = A dimmable light with adjustable color temperature.
thing-type.deconz.consumptionsensor.label = Consumption Sensor 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.label = Daylight Sensor
thing-type.deconz.daylightsensor.description = A daylight sensor
thing-type.deconz.deconz.label = deCONZ thing-type.deconz.deconz.label = deCONZ
thing-type.deconz.deconz.description = A running deCONZ software instance. thing-type.deconz.deconz.description = A running deCONZ software instance.
thing-type.deconz.dimmablelight.label = Dimmable Light 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.label = Doorlock
thing-type.deconz.doorlock.description = A doorlock that can be locked (ON) or unlocked (OFF). 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.label = Color Light
thing-type.deconz.extendedcolorlight.description = A dimmable light with adjustable color. thing-type.deconz.extendedcolorlight.description = A dimmable light with adjustable color.
thing-type.deconz.firesensor.label = Fire Sensor 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.label = Humidity Sensor
thing-type.deconz.humiditysensor.description = A humidity sensor
thing-type.deconz.lightgroup.label = Light Group thing-type.deconz.lightgroup.label = Light Group
thing-type.deconz.lightsensor.label = Light Sensor 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.label = On/Off Light
thing-type.deconz.onofflight.description = A light that can be turned on or off. 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.label = Open/Close Sensor
thing-type.deconz.openclosesensor.description = An open/close sensor
thing-type.deconz.powersensor.label = Power 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.label = Presence Sensor
thing-type.deconz.presencesensor.description = A Presence sensor
thing-type.deconz.pressuresensor.label = Pressure 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.label = Switch/Button
thing-type.deconz.switch.description = A switch or button
thing-type.deconz.temperaturesensor.label = Temperature Sensor 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.label = Thermostat
thing-type.deconz.thermostat.description = A Thermostat sensor/actor thing-type.deconz.thermostat.description = A Thermostat sensor/actor
thing-type.deconz.vibrationsensor.label = Vibration Sensor 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.label = Warning Device
thing-type.deconz.warningdevice.description = A warning device
thing-type.deconz.waterleakagesensor.label = Water Leakage Sensor 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.label = Window Covering
thing-type.deconz.windowcovering.description = A device to cover windows. 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.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.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.label = Host Address
thing-type.config.deconz.bridge.host.description = IP address or host name of deCONZ interface. 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.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.port.description = Port of the deCONZ Websocket.
thing-type.config.deconz.bridge.timeout.label = Timeout 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.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.label = Color Mode
thing-type.config.deconz.colorlight.colormode.description = Override the default color mode (auto-detect) 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.hs = HSB
thing-type.config.deconz.colorlight.colormode.option.xy = XY thing-type.config.deconz.colorlight.colormode.option.xy = XY
thing-type.config.deconz.colorlight.id.label = Device ID 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.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.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.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.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.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.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.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 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.colormode.option.xy = XY
thing-type.config.deconz.lightgroup.id.label = Device ID 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.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.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.id.description = The deCONZ bridge assigns an integer number ID to each device.
thing-type.config.deconz.sensor.lastSeenPolling.label = LastSeen Poll Interval 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 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.label = Alarm
channel-type.deconz.alarm.description = Alarm was triggered. channel-type.deconz.alarm.description = Alarm was triggered.
channel-type.deconz.alert.label = Alert 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.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.label = Carbon-monoxide
channel-type.deconz.carbonmonoxide.description = Carbon-monoxide was detected. 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.label = Consumption
channel-type.deconz.consumption.description = Current consumption channel-type.deconz.consumption.description = Current consumption
channel-type.deconz.ct.label = Color Temperature 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.daylight.description = Light level is above the daylight threshold.
channel-type.deconz.effect.label = Effect Channel channel-type.deconz.effect.label = Effect Channel
channel-type.deconz.effectSpeed.label = Effect Speed 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.label = Fire
channel-type.deconz.fire.description = A fire was detected. channel-type.deconz.fire.description = A fire was detected.
channel-type.deconz.gesture.label = Gesture 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.gesture.state.option.8 = Rotate Counter Clockwise
channel-type.deconz.gestureevent.label = Gesture Trigger 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.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.heatsetpoint.description = Target temperature
channel-type.deconz.humidity.label = Humidity channel-type.deconz.humidity.label = Humidity
channel-type.deconz.humidity.description = Current 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.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.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.label = Lightlevel
channel-type.deconz.light.description = A light level
channel-type.deconz.light.state.option.daylight = Daylight channel-type.deconz.light.state.option.daylight = Daylight
channel-type.deconz.light.state.option.sunset = Sunset channel-type.deconz.light.state.option.sunset = Sunset
channel-type.deconz.light.state.option.dark = Dark 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.label = Illuminance
channel-type.deconz.lightlux.description = Current light illuminance channel-type.deconz.lightlux.description = Current light illuminance
channel-type.deconz.lock.label = Lock 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.label = Mode
channel-type.deconz.mode.description = Current mode channel-type.deconz.mode.description = Current mode
channel-type.deconz.mode.state.option.AUTO = auto channel-type.deconz.mode.state.option.AUTO = auto
channel-type.deconz.mode.state.option.HEAT = heat channel-type.deconz.mode.state.option.HEAT = heat
channel-type.deconz.mode.state.option.OFF = off 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.label = Offset
channel-type.deconz.offset.description = Temperature offset channel-type.deconz.offset.description = Temperature offset
channel-type.deconz.ontime.label = On Time 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.voltage.description = Current voltage
channel-type.deconz.waterleakage.label = Water Leakage channel-type.deconz.waterleakage.label = Water Leakage
channel-type.deconz.waterleakage.description = Water leakage detected channel-type.deconz.waterleakage.description = Water leakage detected
channel-type.deconz.windowopen.label = Window Open
# thing status descriptions # thing status descriptions
offline.light-not-reachable = Not reachable offline.light-not-reachable = Not reachable
offline.sensor-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.

View File

@ -19,6 +19,10 @@
<channel typeId="scene" id="scene"/> <channel typeId="scene" id="scene"/>
</channels> </channels>
<properties>
<property name="thingTypeVersion">1</property>
</properties>
<representation-property>uid</representation-property> <representation-property>uid</representation-property>
<config-description-ref uri="thing-type:deconz:lightgroup"/> <config-description-ref uri="thing-type:deconz:lightgroup"/>
@ -28,6 +32,9 @@
<item-type>Switch</item-type> <item-type>Switch</item-type>
<label>All On</label> <label>All On</label>
<description>"On" if all lights in this group are "On", otherwise "Off".</description> <description>"On" if all lights in this group are "On", otherwise "Off".</description>
<tags>
<tag>Lighting</tag>
</tags>
<state readOnly="true"/> <state readOnly="true"/>
</channel-type> </channel-type>
@ -35,12 +42,18 @@
<item-type>Switch</item-type> <item-type>Switch</item-type>
<label>Any On</label> <label>Any On</label>
<description>"On" if any light in this group is "On", otherwise "Off".</description> <description>"On" if any light in this group is "On", otherwise "Off".</description>
<tags>
<tag>Lighting</tag>
</tags>
<state readOnly="true"/> <state readOnly="true"/>
</channel-type> </channel-type>
<channel-type id="scene"> <channel-type id="scene">
<item-type>String</item-type> <item-type>String</item-type>
<label>Recall Scene</label> <label>Recall Scene</label>
<tags>
<tag>Lighting</tag>
</tags>
</channel-type> </channel-type>
</thing:thing-descriptions> </thing:thing-descriptions>

View File

@ -9,10 +9,9 @@
<bridge-type-ref id="deconz"/> <bridge-type-ref id="deconz"/>
</supported-bridge-type-refs> </supported-bridge-type-refs>
<label>Warning Device</label> <label>Warning Device</label>
<description>A warning device</description>
<category>Siren</category> <category>Siren</category>
<channels> <channels>
<channel id="alert" typeId="alert"></channel> <channel typeId="alert" id="alert"/>
</channels> </channels>
<representation-property>uid</representation-property> <representation-property>uid</representation-property>
@ -40,13 +39,19 @@
<supported-bridge-type-refs> <supported-bridge-type-refs>
<bridge-type-ref id="deconz"/> <bridge-type-ref id="deconz"/>
</supported-bridge-type-refs> </supported-bridge-type-refs>
<label>On/Off Light</label> <label>On/Off Light</label>
<description>A light that can be turned on or off.</description> <description>A light that can be turned on or off.</description>
<channels> <channels>
<channel typeId="system.power" id="switch"/> <channel typeId="system.power" id="switch"/>
<channel typeId="ontime" id="ontime"/> <channel typeId="ontime" id="ontime"/>
</channels> </channels>
<properties>
<property name="thingTypeVersion">1</property>
</properties>
<representation-property>uid</representation-property> <representation-property>uid</representation-property>
<config-description-ref uri="thing-type:deconz:sensor"/> <config-description-ref uri="thing-type:deconz:sensor"/>
@ -57,14 +62,17 @@
<bridge-type-ref id="deconz"/> <bridge-type-ref id="deconz"/>
</supported-bridge-type-refs> </supported-bridge-type-refs>
<label>Dimmable Light</label> <label>Dimmable Light</label>
<description>A dimmable light.</description>
<category>Lightbulb</category> <category>Lightbulb</category>
<channels> <channels>
<channel typeId="system.brightness" id="brightness"/> <channel typeId="system.brightness" id="brightness"/>
<channel typeId="ontime" id="ontime"/> <channel typeId="ontime" id="ontime"/>
<channel id="alert" typeId="alert"></channel> <channel typeId="alert" id="alert"/>
</channels> </channels>
<properties>
<property name="thingTypeVersion">1</property>
</properties>
<representation-property>uid</representation-property> <representation-property>uid</representation-property>
<config-description-ref uri="thing-type:deconz:light"/> <config-description-ref uri="thing-type:deconz:light"/>
@ -81,9 +89,13 @@
<channel typeId="system.brightness" id="brightness"/> <channel typeId="system.brightness" id="brightness"/>
<channel typeId="ct" id="color_temperature"/> <channel typeId="ct" id="color_temperature"/>
<channel typeId="ontime" id="ontime"/> <channel typeId="ontime" id="ontime"/>
<channel id="alert" typeId="alert"></channel> <channel typeId="alert" id="alert"/>
</channels> </channels>
<properties>
<property name="thingTypeVersion">1</property>
</properties>
<representation-property>uid</representation-property> <representation-property>uid</representation-property>
<config-description-ref uri="thing-type:deconz:light"/> <config-description-ref uri="thing-type:deconz:light"/>
@ -99,9 +111,13 @@
<channels> <channels>
<channel typeId="system.color" id="color"/> <channel typeId="system.color" id="color"/>
<channel typeId="ontime" id="ontime"/> <channel typeId="ontime" id="ontime"/>
<channel id="alert" typeId="alert"></channel> <channel typeId="alert" id="alert"/>
</channels> </channels>
<properties>
<property name="thingTypeVersion">1</property>
</properties>
<representation-property>uid</representation-property> <representation-property>uid</representation-property>
<config-description-ref uri="thing-type:deconz:colorlight"/> <config-description-ref uri="thing-type:deconz:colorlight"/>
@ -118,9 +134,13 @@
<channel typeId="system.color" id="color"/> <channel typeId="system.color" id="color"/>
<channel typeId="ct" id="color_temperature"/> <channel typeId="ct" id="color_temperature"/>
<channel typeId="ontime" id="ontime"/> <channel typeId="ontime" id="ontime"/>
<channel id="alert" typeId="alert"></channel> <channel typeId="alert" id="alert"/>
</channels> </channels>
<properties>
<property name="thingTypeVersion">1</property>
</properties>
<representation-property>uid</representation-property> <representation-property>uid</representation-property>
<config-description-ref uri="thing-type:deconz:colorlight"/> <config-description-ref uri="thing-type:deconz:colorlight"/>
@ -161,16 +181,23 @@
<item-type>Number:Time</item-type> <item-type>Number:Time</item-type>
<label>On Time</label> <label>On Time</label>
<description>Time that the light stays on before switched off automatically (0=forever)</description> <description>Time that the light stays on before switched off automatically (0=forever)</description>
<state pattern="%.1f %unit%" min="0"/>
</channel-type> </channel-type>
<channel-type id="effect"> <channel-type id="effect">
<item-type>String</item-type> <item-type>String</item-type>
<label>Effect Channel</label> <label>Effect Channel</label>
<tags>
<tag>Lighting</tag>
</tags>
</channel-type> </channel-type>
<channel-type id="effectSpeed"> <channel-type id="effectSpeed">
<item-type>Number</item-type> <item-type>Number</item-type>
<label>Effect Speed Channel</label> <label>Effect Speed Channel</label>
<tags>
<tag>Lighting</tag>
</tags>
<state min="0" max="10" step="1"/> <state min="0" max="10" step="1"/>
</channel-type> </channel-type>

View File

@ -9,13 +9,16 @@
<bridge-type-ref id="deconz"/> <bridge-type-ref id="deconz"/>
</supported-bridge-type-refs> </supported-bridge-type-refs>
<label>Presence Sensor</label> <label>Presence Sensor</label>
<description>A Presence sensor</description>
<channels> <channels>
<channel typeId="system.motion" id="presence"/> <channel typeId="system.motion" id="presence"/>
<channel typeId="last_updated" id="last_updated"/> <channel typeId="last_updated" id="last_updated"/>
<channel typeId="system.power" id="enabled"/> <channel typeId="system.power" id="enabled"/>
</channels> </channels>
<properties>
<property name="thingTypeVersion">1</property>
</properties>
<representation-property>uid</representation-property> <representation-property>uid</representation-property>
<config-description-ref uri="thing-type:deconz:sensor"/> <config-description-ref uri="thing-type:deconz:sensor"/>
@ -42,7 +45,6 @@
<bridge-type-ref id="deconz"/> <bridge-type-ref id="deconz"/>
</supported-bridge-type-refs> </supported-bridge-type-refs>
<label>Power Sensor</label> <label>Power Sensor</label>
<description>A power sensor</description>
<channels> <channels>
<channel typeId="power" id="power"/> <channel typeId="power" id="power"/>
<channel typeId="last_updated" id="last_updated"/> <channel typeId="last_updated" id="last_updated"/>
@ -58,7 +60,7 @@
<label>Power</label> <label>Power</label>
<description>Current power usage</description> <description>Current power usage</description>
<category>Energy</category> <category>Energy</category>
<state readOnly="true" pattern="%.1f %unit%"></state> <state readOnly="true" pattern="%.1f %unit%"/>
</channel-type> </channel-type>
<channel-type id="voltage"> <channel-type id="voltage">
@ -82,10 +84,9 @@
<bridge-type-ref id="deconz"/> <bridge-type-ref id="deconz"/>
</supported-bridge-type-refs> </supported-bridge-type-refs>
<label>Consumption Sensor</label> <label>Consumption Sensor</label>
<description>A consumption sensor</description>
<channels> <channels>
<channel typeId="consumption" id="consumption"></channel> <channel typeId="consumption" id="consumption"/>
<channel typeId="last_updated" id="last_updated"></channel> <channel typeId="last_updated" id="last_updated"/>
</channels> </channels>
<representation-property>uid</representation-property> <representation-property>uid</representation-property>
@ -98,7 +99,7 @@
<label>Consumption</label> <label>Consumption</label>
<description>Current consumption</description> <description>Current consumption</description>
<category>Energy</category> <category>Energy</category>
<state readOnly="true" pattern="%.1f %unit%"></state> <state readOnly="true" pattern="%.1f %unit%"/>
</channel-type> </channel-type>
<thing-type id="colorcontrol"> <thing-type id="colorcontrol">
@ -113,6 +114,10 @@
<channel typeId="last_updated" id="last_updated"/> <channel typeId="last_updated" id="last_updated"/>
</channels> </channels>
<properties>
<property name="thingTypeVersion">1</property>
</properties>
<representation-property>uid</representation-property> <representation-property>uid</representation-property>
<config-description-ref uri="thing-type:deconz:sensor"/> <config-description-ref uri="thing-type:deconz:sensor"/>
@ -123,7 +128,6 @@
<bridge-type-ref id="deconz"/> <bridge-type-ref id="deconz"/>
</supported-bridge-type-refs> </supported-bridge-type-refs>
<label>Switch/Button</label> <label>Switch/Button</label>
<description>A switch or button</description>
<channels> <channels>
<channel typeId="buttonevent" id="buttonevent"/> <channel typeId="buttonevent" id="buttonevent"/>
<channel typeId="button" id="button"/> <channel typeId="button" id="button"/>
@ -147,7 +151,7 @@
<item-type>Number</item-type> <item-type>Number</item-type>
<label>Button</label> <label>Button</label>
<description>The Button that was last pressed on the switch.</description> <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>
<channel-type id="gestureevent"> <channel-type id="gestureevent">
@ -181,7 +185,6 @@
<bridge-type-ref id="deconz"/> <bridge-type-ref id="deconz"/>
</supported-bridge-type-refs> </supported-bridge-type-refs>
<label>Light Sensor</label> <label>Light Sensor</label>
<description>A light sensor</description>
<channels> <channels>
<channel typeId="lightlux" id="lightlux"/> <channel typeId="lightlux" id="lightlux"/>
<channel typeId="light_level" id="light_level"/> <channel typeId="light_level" id="light_level"/>
@ -199,7 +202,7 @@
<item-type>Number:Illuminance</item-type> <item-type>Number:Illuminance</item-type>
<label>Illuminance</label> <label>Illuminance</label>
<description>Current light illuminance</description> <description>Current light illuminance</description>
<state readOnly="true" pattern="%.1f %unit%"></state> <state readOnly="true" pattern="%.1f %unit%"/>
</channel-type> </channel-type>
<channel-type id="light_level" advanced="true"> <channel-type id="light_level" advanced="true">
@ -228,7 +231,6 @@
<bridge-type-ref id="deconz"/> <bridge-type-ref id="deconz"/>
</supported-bridge-type-refs> </supported-bridge-type-refs>
<label>Temperature Sensor</label> <label>Temperature Sensor</label>
<description>A temperature sensor</description>
<channels> <channels>
<channel typeId="temperature" id="temperature"/> <channel typeId="temperature" id="temperature"/>
<channel typeId="last_updated" id="last_updated"/> <channel typeId="last_updated" id="last_updated"/>
@ -244,7 +246,7 @@
<label>Temperature</label> <label>Temperature</label>
<description>Current temperature</description> <description>Current temperature</description>
<category>Temperature</category> <category>Temperature</category>
<state readOnly="true" pattern="%.2f %unit%"></state> <state readOnly="true" pattern="%.2f %unit%"/>
</channel-type> </channel-type>
<thing-type id="humiditysensor"> <thing-type id="humiditysensor">
@ -252,7 +254,6 @@
<bridge-type-ref id="deconz"/> <bridge-type-ref id="deconz"/>
</supported-bridge-type-refs> </supported-bridge-type-refs>
<label>Humidity Sensor</label> <label>Humidity Sensor</label>
<description>A humidity sensor</description>
<channels> <channels>
<channel typeId="humidity" id="humidity"/> <channel typeId="humidity" id="humidity"/>
<channel typeId="last_updated" id="last_updated"/> <channel typeId="last_updated" id="last_updated"/>
@ -268,7 +269,7 @@
<label>Humidity</label> <label>Humidity</label>
<description>Current humidity</description> <description>Current humidity</description>
<category>Humidity</category> <category>Humidity</category>
<state readOnly="true" pattern="%.2f %unit%"></state> <state readOnly="true" pattern="%.2f %unit%"/>
</channel-type> </channel-type>
<thing-type id="pressuresensor"> <thing-type id="pressuresensor">
@ -276,10 +277,9 @@
<bridge-type-ref id="deconz"/> <bridge-type-ref id="deconz"/>
</supported-bridge-type-refs> </supported-bridge-type-refs>
<label>Pressure Sensor</label> <label>Pressure Sensor</label>
<description>A pressure senor</description>
<channels> <channels>
<channel typeId="pressure" id="pressure"></channel> <channel typeId="pressure" id="pressure"/>
<channel typeId="last_updated" id="last_updated"></channel> <channel typeId="last_updated" id="last_updated"/>
</channels> </channels>
<representation-property>uid</representation-property> <representation-property>uid</representation-property>
@ -292,7 +292,7 @@
<label>Pressure</label> <label>Pressure</label>
<description>Current pressure</description> <description>Current pressure</description>
<category>Pressure</category> <category>Pressure</category>
<state readOnly="true" pattern="%.1f %unit%"></state> <state readOnly="true" pattern="%.1f %unit%"/>
</channel-type> </channel-type>
<thing-type id="daylightsensor"> <thing-type id="daylightsensor">
@ -300,10 +300,9 @@
<bridge-type-ref id="deconz"/> <bridge-type-ref id="deconz"/>
</supported-bridge-type-refs> </supported-bridge-type-refs>
<label>Daylight Sensor</label> <label>Daylight Sensor</label>
<description>A daylight sensor</description>
<channels> <channels>
<channel typeId="value" id="value"></channel> <channel typeId="value" id="value"/>
<channel typeId="light" id="light"></channel> <channel typeId="light" id="light"/>
</channels> </channels>
<representation-property>uid</representation-property> <representation-property>uid</representation-property>
@ -315,13 +314,12 @@
<item-type>Number</item-type> <item-type>Number</item-type>
<label>Daylight Value</label> <label>Daylight Value</label>
<description>Dawn is around 130, sunrise at 140, sunset at 190, and dusk at 210</description> <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>
<channel-type id="light"> <channel-type id="light">
<item-type>String</item-type> <item-type>String</item-type>
<label>Lightlevel</label> <label>Lightlevel</label>
<description>A light level</description>
<state readOnly="true"> <state readOnly="true">
<options> <options>
<option value="daylight">Daylight</option> <option value="daylight">Daylight</option>
@ -336,7 +334,6 @@
<bridge-type-ref id="deconz"/> <bridge-type-ref id="deconz"/>
</supported-bridge-type-refs> </supported-bridge-type-refs>
<label>Open/Close Sensor</label> <label>Open/Close Sensor</label>
<description>An open/close sensor</description>
<channels> <channels>
<channel typeId="open" id="open"/> <channel typeId="open" id="open"/>
<channel typeId="last_updated" id="last_updated"/> <channel typeId="last_updated" id="last_updated"/>
@ -351,7 +348,7 @@
<item-type>Contact</item-type> <item-type>Contact</item-type>
<label>Open/Close</label> <label>Open/Close</label>
<description>Open/Close detected</description> <description>Open/Close detected</description>
<state readOnly="true"></state> <state readOnly="true"/>
</channel-type> </channel-type>
<thing-type id="waterleakagesensor"> <thing-type id="waterleakagesensor">
@ -359,7 +356,6 @@
<bridge-type-ref id="deconz"/> <bridge-type-ref id="deconz"/>
</supported-bridge-type-refs> </supported-bridge-type-refs>
<label>Water Leakage Sensor</label> <label>Water Leakage Sensor</label>
<description>A water leakage sensor</description>
<channels> <channels>
<channel typeId="waterleakage" id="waterleakage"/> <channel typeId="waterleakage" id="waterleakage"/>
<channel typeId="last_updated" id="last_updated"/> <channel typeId="last_updated" id="last_updated"/>
@ -382,7 +378,6 @@
<bridge-type-ref id="deconz"/> <bridge-type-ref id="deconz"/>
</supported-bridge-type-refs> </supported-bridge-type-refs>
<label>Fire Sensor</label> <label>Fire Sensor</label>
<description>A fire sensor</description>
<channels> <channels>
<channel typeId="fire" id="fire"/> <channel typeId="fire" id="fire"/>
<channel typeId="last_updated" id="last_updated"/> <channel typeId="last_updated" id="last_updated"/>
@ -405,7 +400,6 @@
<bridge-type-ref id="deconz"/> <bridge-type-ref id="deconz"/>
</supported-bridge-type-refs> </supported-bridge-type-refs>
<label>Alarm Sensor</label> <label>Alarm Sensor</label>
<description>An alarm sensor</description>
<channels> <channels>
<channel typeId="alarm" id="alarm"/> <channel typeId="alarm" id="alarm"/>
<channel typeId="last_updated" id="last_updated"/> <channel typeId="last_updated" id="last_updated"/>
@ -435,7 +429,6 @@
<bridge-type-ref id="deconz"/> <bridge-type-ref id="deconz"/>
</supported-bridge-type-refs> </supported-bridge-type-refs>
<label>Vibration Sensor</label> <label>Vibration Sensor</label>
<description>A vibration sensor</description>
<channels> <channels>
<channel typeId="vibration" id="vibration"/> <channel typeId="vibration" id="vibration"/>
<channel typeId="last_updated" id="last_updated"/> <channel typeId="last_updated" id="last_updated"/>
@ -458,12 +451,15 @@
<bridge-type-ref id="deconz"/> <bridge-type-ref id="deconz"/>
</supported-bridge-type-refs> </supported-bridge-type-refs>
<label>Battery Sensor</label> <label>Battery Sensor</label>
<description>A battery sensor</description>
<channels> <channels>
<channel typeId="system.battery-level" id="battery_level"/> <channel typeId="system.battery-level" id="battery_level"/>
<channel typeId="last_updated" id="last_updated"/> <channel typeId="last_updated" id="last_updated"/>
</channels> </channels>
<properties>
<property name="thingTypeVersion">1</property>
</properties>
<representation-property>uid</representation-property> <representation-property>uid</representation-property>
<config-description-ref uri="thing-type:deconz:sensor"/> <config-description-ref uri="thing-type:deconz:sensor"/>
@ -491,13 +487,11 @@
<state readOnly="true"/> <state readOnly="true"/>
</channel-type> </channel-type>
<thing-type id="airqualitysensor"> <thing-type id="airqualitysensor">
<supported-bridge-type-refs> <supported-bridge-type-refs>
<bridge-type-ref id="deconz"/> <bridge-type-ref id="deconz"/>
</supported-bridge-type-refs> </supported-bridge-type-refs>
<label>Air quality Sensor</label> <label>Carbon-monoxide Sensor</label>
<description>An air quality sensor</description>
<channels> <channels>
<channel typeId="airquality" id="airquality"/> <channel typeId="airquality" id="airquality"/>
<channel typeId="airqualityppb" id="airqualityppb"/> <channel typeId="airqualityppb" id="airqualityppb"/>
@ -511,20 +505,41 @@
<channel-type id="airquality"> <channel-type id="airquality">
<item-type>String</item-type> <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>Current air quality level based on volatile organic compounds (VOCs) measurement. Example: good or poor,
...</description> ...</description>
<state readOnly="true" pattern="%s"></state> <state readOnly="true"/>
</channel-type> </channel-type>
<channel-type id="airqualityppb"> <channel-type id="airqualityppb">
<item-type>Number:Dimensionless</item-type> <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 <description>Current air quality based on measurements of volatile organic compounds (VOCs). The measured value is
specified in ppb (parts per billion).</description> specified in ppb (parts per billion).</description>
<state readOnly="true" pattern="%d"></state> <state readOnly="true"/>
</channel-type> </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"> <thing-type id="thermostat">
<supported-bridge-type-refs> <supported-bridge-type-refs>
@ -544,12 +559,26 @@
<config-description-ref uri="thing-type:deconz:sensor"/> <config-description-ref uri="thing-type:deconz:sensor"/>
</thing-type> </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"> <channel-type id="heatsetpoint">
<item-type>Number:Temperature</item-type> <item-type>Number:Temperature</item-type>
<label>Target Temperature</label> <label>Target Temperature</label>
<description>Target temperature</description> <description>Target temperature</description>
<category>Heating</category> <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>
<channel-type id="mode"> <channel-type id="mode">
<item-type>String</item-type> <item-type>String</item-type>
@ -568,13 +597,13 @@
<item-type>Number:Temperature</item-type> <item-type>Number:Temperature</item-type>
<label>Offset</label> <label>Offset</label>
<description>Temperature offset</description> <description>Temperature offset</description>
<state pattern="%.2f %unit%" step="0.01"></state> <state pattern="%.2f %unit%" step="0.01"/>
</channel-type> </channel-type>
<channel-type id="valve"> <channel-type id="valve">
<item-type>Number:Dimensionless</item-type> <item-type>Number:Dimensionless</item-type>
<label>Valve position</label> <label>Valve position</label>
<description>Current valve position</description> <description>Current valve position</description>
<state readOnly="true" pattern="%.0f %unit%"/> <state readOnly="true" pattern="%.1f %unit%"/>
</channel-type> </channel-type>
</thing:thing-descriptions> </thing:thing-descriptions>

View File

@ -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>

View File

@ -60,7 +60,7 @@ import com.google.gson.GsonBuilder;
* @author Jan N. Klug - Initial contribution * @author Jan N. Klug - Initial contribution
*/ */
@ExtendWith(MockitoExtension.class) @ExtendWith(MockitoExtension.class)
@MockitoSettings(strictness = Strictness.LENIENT) @MockitoSettings(strictness = Strictness.WARN)
@NonNullByDefault @NonNullByDefault
public class DeconzTest { public class DeconzTest {
private @NonNullByDefault({}) Gson gson; private @NonNullByDefault({}) Gson gson;

View File

@ -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));
}
}

View File

@ -15,6 +15,7 @@ package org.openhab.binding.deconz;
import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.mockito.ArgumentMatchers.*; import static org.mockito.ArgumentMatchers.*;
import static org.openhab.binding.deconz.internal.BindingConstants.*; 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.io.IOException;
import java.util.HashMap; import java.util.HashMap;
@ -77,34 +78,36 @@ public class LightsTest {
assertNotNull(lightMessage); assertNotNull(lightMessage);
ThingUID thingUID = new ThingUID("deconz", "light"); ThingUID thingUID = new ThingUID("deconz", "light");
ChannelUID channelUID_bri = new ChannelUID(thingUID, CHANNEL_BRIGHTNESS); ChannelUID channelUIDBri = new ChannelUID(thingUID, CHANNEL_BRIGHTNESS);
ChannelUID channelUID_ct = new ChannelUID(thingUID, CHANNEL_COLOR_TEMPERATURE); ChannelUID channelUIDCt = new ChannelUID(thingUID, CHANNEL_COLOR_TEMPERATURE);
Thing light = ThingBuilder.create(THING_TYPE_COLOR_TEMPERATURE_LIGHT, thingUID) Thing light = ThingBuilder.create(THING_TYPE_COLOR_TEMPERATURE_LIGHT, thingUID)
.withChannel(ChannelBuilder.create(channelUID_bri, "Dimmer").build()) .withProperties(Map.of(PROPERTY_THING_TYPE_VERSION, "1"))
.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, LightThingHandler lightThingHandler = new LightThingHandler(light, gson, stateDescriptionProvider,
commandDescriptionProvider); commandDescriptionProvider);
lightThingHandler.setCallback(thingHandlerCallback); lightThingHandler.setCallback(thingHandlerCallback);
lightThingHandler.messageReceived("", lightMessage); lightThingHandler.messageReceived(lightMessage);
Mockito.verify(thingHandlerCallback).stateUpdated(eq(channelUID_bri), eq(new PercentType("21"))); Mockito.verify(thingHandlerCallback).stateUpdated(eq(channelUIDBri), eq(new PercentType("21")));
Mockito.verify(thingHandlerCallback).stateUpdated(eq(channelUID_ct), eq(new DecimalType("2500"))); Mockito.verify(thingHandlerCallback).stateUpdated(eq(channelUIDCt), eq(new DecimalType("2500")));
} }
@Test @Test
public void colorTemperatureLightStateDescriptionProviderTest() { public void colorTemperatureLightStateDescriptionProviderTest() {
ThingUID thingUID = new ThingUID("deconz", "light"); ThingUID thingUID = new ThingUID("deconz", "light");
ChannelUID channelUID_bri = new ChannelUID(thingUID, CHANNEL_BRIGHTNESS); ChannelUID channelUIDBri = new ChannelUID(thingUID, CHANNEL_BRIGHTNESS);
ChannelUID channelUID_ct = new ChannelUID(thingUID, CHANNEL_COLOR_TEMPERATURE); ChannelUID channelUIDCt = new ChannelUID(thingUID, CHANNEL_COLOR_TEMPERATURE);
Map<String, String> properties = new HashMap<>(); Map<String, String> properties = new HashMap<>();
properties.put(PROPERTY_CT_MAX, "500"); properties.put(PROPERTY_CT_MAX, "500");
properties.put(PROPERTY_CT_MIN, "200"); 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) Thing light = ThingBuilder.create(THING_TYPE_COLOR_TEMPERATURE_LIGHT, thingUID).withProperties(properties)
.withChannel(ChannelBuilder.create(channelUID_bri, "Dimmer").build()) .withChannel(ChannelBuilder.create(channelUIDBri, "Dimmer").build())
.withChannel(ChannelBuilder.create(channelUID_ct, "Number").build()).build(); .withChannel(ChannelBuilder.create(channelUIDCt, "Number").build()).build();
LightThingHandler lightThingHandler = new LightThingHandler(light, gson, stateDescriptionProvider, LightThingHandler lightThingHandler = new LightThingHandler(light, gson, stateDescriptionProvider,
commandDescriptionProvider) { commandDescriptionProvider) {
// avoid warning when initializing // avoid warning when initializing
@ -116,7 +119,7 @@ public class LightsTest {
lightThingHandler.initialize(); lightThingHandler.initialize();
Mockito.verify(stateDescriptionProvider).setDescriptionFragment(eq(channelUID_ct), any()); Mockito.verify(stateDescriptionProvider).setDescriptionFragment(eq(channelUIDCt), any());
} }
@Test @Test
@ -125,16 +128,17 @@ public class LightsTest {
assertNotNull(lightMessage); assertNotNull(lightMessage);
ThingUID thingUID = new ThingUID("deconz", "light"); 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) 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, LightThingHandler lightThingHandler = new LightThingHandler(light, gson, stateDescriptionProvider,
commandDescriptionProvider); commandDescriptionProvider);
lightThingHandler.setCallback(thingHandlerCallback); lightThingHandler.setCallback(thingHandlerCallback);
lightThingHandler.messageReceived("", lightMessage); lightThingHandler.messageReceived(lightMessage);
Mockito.verify(thingHandlerCallback).stateUpdated(eq(channelUID_bri), eq(new PercentType("38"))); Mockito.verify(thingHandlerCallback).stateUpdated(eq(channelUIDBri), eq(new PercentType("38")));
} }
@Test @Test
@ -143,16 +147,17 @@ public class LightsTest {
assertNotNull(lightMessage); assertNotNull(lightMessage);
ThingUID thingUID = new ThingUID("deconz", "light"); 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) 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, LightThingHandler lightThingHandler = new LightThingHandler(light, gson, stateDescriptionProvider,
commandDescriptionProvider); commandDescriptionProvider);
lightThingHandler.setCallback(thingHandlerCallback); lightThingHandler.setCallback(thingHandlerCallback);
lightThingHandler.messageReceived("", lightMessage); lightThingHandler.messageReceived(lightMessage);
Mockito.verify(thingHandlerCallback).stateUpdated(eq(channelUID_bri), eq(new PercentType("100"))); Mockito.verify(thingHandlerCallback).stateUpdated(eq(channelUIDBri), eq(new PercentType("100")));
} }
@Test @Test
@ -161,16 +166,17 @@ public class LightsTest {
assertNotNull(lightMessage); assertNotNull(lightMessage);
ThingUID thingUID = new ThingUID("deconz", "light"); 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) 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, LightThingHandler lightThingHandler = new LightThingHandler(light, gson, stateDescriptionProvider,
commandDescriptionProvider); commandDescriptionProvider);
lightThingHandler.setCallback(thingHandlerCallback); lightThingHandler.setCallback(thingHandlerCallback);
lightThingHandler.messageReceived("", lightMessage); lightThingHandler.messageReceived(lightMessage);
Mockito.verify(thingHandlerCallback).stateUpdated(eq(channelUID_bri), eq(new PercentType("0"))); Mockito.verify(thingHandlerCallback).stateUpdated(eq(channelUIDBri), eq(new PercentType("0")));
} }
@Test @Test
@ -179,15 +185,16 @@ public class LightsTest {
assertNotNull(lightMessage); assertNotNull(lightMessage);
ThingUID thingUID = new ThingUID("deconz", "light"); 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) 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, LightThingHandler lightThingHandler = new LightThingHandler(light, gson, stateDescriptionProvider,
commandDescriptionProvider); commandDescriptionProvider);
lightThingHandler.setCallback(thingHandlerCallback); lightThingHandler.setCallback(thingHandlerCallback);
lightThingHandler.messageReceived("", lightMessage); lightThingHandler.messageReceived(lightMessage);
Mockito.verify(thingHandlerCallback).stateUpdated(eq(channelUID_pos), eq(new PercentType("41"))); Mockito.verify(thingHandlerCallback).stateUpdated(eq(channelUIDPos), eq(new PercentType("41")));
} }
} }

View File

@ -44,6 +44,7 @@ import org.openhab.core.thing.ThingUID;
import org.openhab.core.thing.binding.ThingHandlerCallback; import org.openhab.core.thing.binding.ThingHandlerCallback;
import org.openhab.core.thing.binding.builder.ChannelBuilder; import org.openhab.core.thing.binding.builder.ChannelBuilder;
import org.openhab.core.thing.binding.builder.ThingBuilder; import org.openhab.core.thing.binding.builder.ThingBuilder;
import org.openhab.core.types.UnDefType;
import com.google.gson.Gson; import com.google.gson.Gson;
import com.google.gson.GsonBuilder; import com.google.gson.GsonBuilder;
@ -82,7 +83,7 @@ public class SensorsTest {
SensorThingHandler sensorThingHandler = new SensorThingHandler(sensor, gson); SensorThingHandler sensorThingHandler = new SensorThingHandler(sensor, gson);
sensorThingHandler.setCallback(thingHandlerCallback); sensorThingHandler.setCallback(thingHandlerCallback);
sensorThingHandler.messageReceived("", sensorMessage); sensorThingHandler.messageReceived(sensorMessage);
Mockito.verify(thingHandlerCallback).stateUpdated(eq(channelUID), eq(OnOffType.ON)); Mockito.verify(thingHandlerCallback).stateUpdated(eq(channelUID), eq(OnOffType.ON));
} }
@ -100,7 +101,7 @@ public class SensorsTest {
sensorThingHandler.setCallback(thingHandlerCallback); sensorThingHandler.setCallback(thingHandlerCallback);
// ACT // ACT
sensorThingHandler.messageReceived("", sensorMessage); sensorThingHandler.messageReceived(sensorMessage);
// ASSERT // ASSERT
Mockito.verify(thingHandlerCallback).stateUpdated(eq(channelUID), eq(StringType.valueOf("good"))); Mockito.verify(thingHandlerCallback).stateUpdated(eq(channelUID), eq(StringType.valueOf("good")));
@ -120,10 +121,10 @@ public class SensorsTest {
sensorThingHandler.setCallback(thingHandlerCallback); sensorThingHandler.setCallback(thingHandlerCallback);
// ACT // ACT
sensorThingHandler.messageReceived("", sensorMessage); sensorThingHandler.messageReceived(sensorMessage);
// ASSERT // ASSERT
Mockito.verify(thingHandlerCallback).stateUpdated(eq(channelUID), eq(new DecimalType(129))); Mockito.verify(thingHandlerCallback).stateUpdated(eq(channelUID), eq(new QuantityType<>("129 ppb")));
} }
@Test @Test
@ -144,15 +145,23 @@ public class SensorsTest {
SensorThermostatThingHandler sensorThingHandler = new SensorThermostatThingHandler(sensor, gson); SensorThermostatThingHandler sensorThingHandler = new SensorThermostatThingHandler(sensor, gson);
sensorThingHandler.setCallback(thingHandlerCallback); sensorThingHandler.setCallback(thingHandlerCallback);
sensorThingHandler.messageReceived("", sensorMessage); sensorMessage = DeconzTest.getObjectFromJson("thermostat-undef.json", SensorMessage.class, gson);
Mockito.verify(thingHandlerCallback).stateUpdated(eq(channelValveUID), assertNotNull(sensorMessage);
eq(new QuantityType<>(100.0, Units.PERCENT))); sensorThingHandler.messageReceived(sensorMessage);
Mockito.verify(thingHandlerCallback).stateUpdated(eq(channelValveUID), eq(UnDefType.UNDEF));
Mockito.verify(thingHandlerCallback).stateUpdated(eq(channelHeatSetPointUID), Mockito.verify(thingHandlerCallback).stateUpdated(eq(channelHeatSetPointUID),
eq(new QuantityType<>(25, SIUnits.CELSIUS))); eq(new QuantityType<>(25, SIUnits.CELSIUS)));
Mockito.verify(thingHandlerCallback).stateUpdated(eq(channelModeUID), Mockito.verify(thingHandlerCallback).stateUpdated(eq(channelModeUID),
eq(new StringType(ThermostatMode.AUTO.name()))); eq(new StringType(ThermostatMode.AUTO.name())));
Mockito.verify(thingHandlerCallback).stateUpdated(eq(channelTemperatureUID), Mockito.verify(thingHandlerCallback).stateUpdated(eq(channelTemperatureUID),
eq(new QuantityType<>(16.5, SIUnits.CELSIUS))); 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 @Test
@ -174,7 +183,7 @@ public class SensorsTest {
SensorThingHandler sensorThingHandler = new SensorThingHandler(sensor, gson); SensorThingHandler sensorThingHandler = new SensorThingHandler(sensor, gson);
sensorThingHandler.setCallback(thingHandlerCallback); sensorThingHandler.setCallback(thingHandlerCallback);
sensorThingHandler.messageReceived("", sensorMessage); sensorThingHandler.messageReceived(sensorMessage);
Mockito.verify(thingHandlerCallback).stateUpdated(eq(channelFireUID), eq(OnOffType.OFF)); Mockito.verify(thingHandlerCallback).stateUpdated(eq(channelFireUID), eq(OnOffType.OFF));
Mockito.verify(thingHandlerCallback).stateUpdated(eq(channelBatteryLevelUID), eq(new DecimalType(98))); Mockito.verify(thingHandlerCallback).stateUpdated(eq(channelBatteryLevelUID), eq(new DecimalType(98)));

View File

@ -3,7 +3,7 @@
"battery": 98, "battery": 98,
"on": true, "on": true,
"pending" : [], "pending" : [],
"reachable": false "reachable": true
}, },
"ep": 1, "ep": 1,
"etag": "717549a99371f3ea1a5f0b40f1537094", "etag": "717549a99371f3ea1a5f0b40f1537094",

View File

@ -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"
}

View File

@ -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"
}

View File

@ -19,7 +19,7 @@
"lastupdated": "2020-05-31T20:24:55.819", "lastupdated": "2020-05-31T20:24:55.819",
"on": true, "on": true,
"temperature": 1650, "temperature": 1650,
"valve": 255 "valve": 99
}, },
"swversion": "20191014", "swversion": "20191014",
"type": "ZHAThermostat", "type": "ZHAThermostat",