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