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

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

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

View File

@ -64,7 +64,7 @@
/bundles/org.openhab.binding.dali/ @rs22
/bundles/org.openhab.binding.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

View File

@ -9,27 +9,28 @@ deCONZ offers a documented real-time channel that this binding makes use of to b
There is one bridge (`deconz`) that manages the connection to the deCONZ software instance.
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.

View File

@ -15,6 +15,7 @@ package org.openhab.binding.deconz.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.library.types.PercentType;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.type.ChannelTypeUID;
/**
* The {@link BindingConstants} class defines common constants, which are
@ -50,6 +51,8 @@ public class BindingConstants {
public static final ThingTypeUID THING_TYPE_CARBONMONOXIDE_SENSOR = new ThingTypeUID(BINDING_ID,
"carbonmonoxidesensor");
public static final ThingTypeUID THING_TYPE_AIRQUALITY_SENSOR = new ThingTypeUID(BINDING_ID, "airqualitysensor");
public static final ThingTypeUID THING_TYPE_MOISTURE_SENSOR = new ThingTypeUID(BINDING_ID, "moisturesensor");
// Special sensor - Thermostat
public static final ThingTypeUID THING_TYPE_THERMOSTAT = new ThingTypeUID(BINDING_ID, "thermostat");
@ -75,6 +78,7 @@ public class BindingConstants {
public static final String CHANNEL_LAST_SEEN = "last_seen";
public static final String CHANNEL_POWER = "power";
public static final String CHANNEL_CONSUMPTION = "consumption";
public static final String CHANNEL_CONSUMPTION_2 = "consumption2";
public static final String CHANNEL_VOLTAGE = "voltage";
public static final String CHANNEL_CURRENT = "current";
public static final String CHANNEL_VALUE = "value";
@ -101,11 +105,14 @@ public class BindingConstants {
public static final String CHANNEL_CARBONMONOXIDE = "carbonmonoxide";
public static final String CHANNEL_AIRQUALITY = "airquality";
public static final String CHANNEL_AIRQUALITYPPB = "airqualityppb";
public static final String CHANNEL_MOISTURE = "moisture";
public static final String CHANNEL_HEATSETPOINT = "heatsetpoint";
public static final String CHANNEL_THERMOSTAT_MODE = "mode";
public static final String CHANNEL_THERMOSTAT_LOCKED = "locked";
public static final String CHANNEL_TEMPERATURE_OFFSET = "offset";
public static final String CHANNEL_VALVE_POSITION = "valve";
public static final String CHANNEL_WINDOWOPEN = "windowopen";
public static final String CHANNEL_WINDOW_OPEN = "windowopen";
public static final String CHANNEL_EXTERNAL_WINDOW_OPEN = "externalwindowopen";
// group + light channel ids
public static final String CHANNEL_SWITCH = "switch";
@ -122,6 +129,11 @@ public class BindingConstants {
public static final String CHANNEL_SCENE = "scene";
public static final String CHANNEL_ONTIME = "ontime";
// channel uids
public static final ChannelTypeUID CHANNEL_EFFECT_TYPE_UID = new ChannelTypeUID(BINDING_ID, CHANNEL_EFFECT);
public static final ChannelTypeUID CHANNEL_EFFECT_SPEED_TYPE_UID = new ChannelTypeUID(BINDING_ID,
CHANNEL_EFFECT_SPEED);
// Thing configuration
public static final String CONFIG_HOST = "host";
public static final String CONFIG_HTTP_PORT = "httpPort";

View File

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

View File

@ -19,16 +19,20 @@ import java.util.concurrent.ConcurrentHashMap;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.events.EventPublisher;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.thing.binding.BaseDynamicStateDescriptionProvider;
import org.openhab.core.thing.events.ThingEventFactory;
import org.openhab.core.thing.i18n.ChannelTypeI18nLocalizationService;
import org.openhab.core.thing.link.ItemChannelLinkRegistry;
import org.openhab.core.thing.type.DynamicStateDescriptionProvider;
import org.openhab.core.types.StateDescription;
import org.openhab.core.types.StateDescriptionFragment;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -45,6 +49,15 @@ public class DeconzDynamicStateDescriptionProvider extends BaseDynamicStateDescr
private final Map<ChannelUID, StateDescriptionFragment> stateDescriptionFragments = new ConcurrentHashMap<>();
@Activate
public DeconzDynamicStateDescriptionProvider(final @Reference EventPublisher eventPublisher, //
final @Reference ItemChannelLinkRegistry itemChannelLinkRegistry, //
final @Reference ChannelTypeI18nLocalizationService channelTypeI18nLocalizationService) {
this.eventPublisher = eventPublisher;
this.itemChannelLinkRegistry = itemChannelLinkRegistry;
this.channelTypeI18nLocalizationService = channelTypeI18nLocalizationService;
}
/**
* Set a state description for a channel. This description will be used when preparing the channel state by
* the framework for presentation. A previous description, if existed, will be replaced.
@ -59,9 +72,10 @@ public class DeconzDynamicStateDescriptionProvider extends BaseDynamicStateDescr
if (!stateDescriptionFragment.equals(oldStateDescriptionFragment)) {
logger.trace("adding state description for channel {}", channelUID);
stateDescriptionFragments.put(channelUID, stateDescriptionFragment);
ItemChannelLinkRegistry itemChannelLinkRegistry = this.itemChannelLinkRegistry;
ItemChannelLinkRegistry localItemChannelLinkRegistry = itemChannelLinkRegistry;
postEvent(ThingEventFactory.createChannelDescriptionChangedEvent(channelUID,
itemChannelLinkRegistry != null ? itemChannelLinkRegistry.getLinkedItemNames(channelUID) : Set.of(),
localItemChannelLinkRegistry != null ? localItemChannelLinkRegistry.getLinkedItemNames(channelUID)
: Set.of(),
stateDescriptionFragment, oldStateDescriptionFragment));
}
}

View File

@ -19,9 +19,11 @@ import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.library.types.DateTimeType;
import org.openhab.core.library.types.PercentType;
@ -59,7 +61,7 @@ public class Util {
}
/**
* convert a brightness value from int to PercentType
* Convert a brightness value from int to PercentType
*
* @param val the value
* @return the corresponding PercentType value
@ -67,11 +69,11 @@ public class Util {
public static PercentType toPercentType(int val) {
int scaledValue = (int) Math.ceil(val / BRIGHTNESS_FACTOR);
return new PercentType(
Util.constrainToRange(scaledValue, PercentType.ZERO.intValue(), PercentType.HUNDRED.intValue()));
constrainToRange(scaledValue, PercentType.ZERO.intValue(), PercentType.HUNDRED.intValue()));
}
/**
* convert a brightness value from PercentType to int
* Convert a brightness value from PercentType to int
*
* @param val the value
* @return the corresponding int value
@ -81,7 +83,7 @@ public class Util {
}
/**
* convert a timestamp string to a DateTimeType
* Convert a timestamp string to a DateTimeType
*
* @param timestamp either in zoned date time or local date time format
* @return the corresponding DateTimeType
@ -95,4 +97,15 @@ public class Util {
ZoneOffset.UTC, ZoneId.systemDefault()));
}
}
/**
* Get all keys corresponding to a given value of a map
*
* @param map a map
* @param value the value to find in the map
* @return Stream of all keys for the value
*/
public static <@NonNull K, @NonNull V> Stream<K> getKeysFromValue(Map<K, V> map, V value) {
return map.entrySet().stream().filter(e -> e.getValue().equals(value)).map(Map.Entry::getKey);
}
}

View File

@ -0,0 +1,87 @@
/**
* Copyright (c) 2010-2023 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.deconz.internal.action;
import java.util.Map;
import java.util.Objects;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.http.HttpMethod;
import org.openhab.binding.deconz.internal.Util;
import org.openhab.binding.deconz.internal.handler.DeconzBridgeHandler;
import org.openhab.core.automation.annotation.ActionInput;
import org.openhab.core.automation.annotation.RuleAction;
import org.openhab.core.thing.binding.ThingActions;
import org.openhab.core.thing.binding.ThingActionsScope;
import org.openhab.core.thing.binding.ThingHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link BridgeActions} provides actions for managing scenes in groups
*
* @author Jan N. Klug - Initial contribution
*/
@ThingActionsScope(name = "deconz")
@NonNullByDefault
public class BridgeActions implements ThingActions {
private final Logger logger = LoggerFactory.getLogger(BridgeActions.class);
private @Nullable DeconzBridgeHandler handler;
@RuleAction(label = "@text/action.permit-join-network.label", description = "@text/action.permit-join-network.description")
public void permitJoin(
@ActionInput(name = "duration", label = "@text/action.permit-join-network.duration.label", description = "@text/action.permit-join-network.duration.description") @Nullable Integer duration) {
DeconzBridgeHandler handler = this.handler;
if (handler == null) {
logger.warn("Deconz BridgeActions service ThingHandler is null!");
return;
}
int searchDuration = Util.constrainToRange(Objects.requireNonNullElse(duration, 120), 1, 240);
Object object = Map.of("permitjoin", searchDuration);
handler.sendObject("config", object, HttpMethod.PUT).thenAccept(v -> {
if (v.getResponseCode() != java.net.HttpURLConnection.HTTP_OK) {
logger.warn("Sending {} via PUT to config failed: {} - {}", object, v.getResponseCode(), v.getBody());
} else {
logger.trace("Result code={}, body={}", v.getResponseCode(), v.getBody());
logger.info("Enabled device searching for {} seconds on bridge {}.", searchDuration,
handler.getThing().getUID());
}
}).exceptionally(e -> {
logger.warn("Sending {} via PUT to config failed: {} - {}", object, e.getClass(), e.getMessage());
return null;
});
}
public static void permitJoin(ThingActions actions, @Nullable Integer duration) {
if (actions instanceof BridgeActions bridgeActions) {
bridgeActions.permitJoin(duration);
}
}
@Override
public void setThingHandler(@Nullable ThingHandler handler) {
if (handler instanceof DeconzBridgeHandler) {
this.handler = (DeconzBridgeHandler) handler;
}
}
@Override
public @Nullable ThingHandler getThingHandler() {
return handler;
}
}

View File

@ -0,0 +1,156 @@
/**
* Copyright (c) 2010-2023 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.deconz.internal.action;
import java.lang.reflect.Type;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.http.HttpMethod;
import org.openhab.binding.deconz.internal.dto.NewSceneResponse;
import org.openhab.binding.deconz.internal.handler.GroupThingHandler;
import org.openhab.core.automation.annotation.ActionInput;
import org.openhab.core.automation.annotation.ActionOutput;
import org.openhab.core.automation.annotation.RuleAction;
import org.openhab.core.thing.binding.ThingActions;
import org.openhab.core.thing.binding.ThingActionsScope;
import org.openhab.core.thing.binding.ThingHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
import com.google.gson.JsonParseException;
import com.google.gson.reflect.TypeToken;
/**
* The {@link GroupActions} provides actions for managing scenes in groups
*
* @author Jan N. Klug - Initial contribution
*/
@ThingActionsScope(name = "deconz")
@NonNullByDefault
public class GroupActions implements ThingActions {
private static final String NEW_SCENE_ID_OUTPUT = "newSceneId";
private static final Type NEW_SCENE_RESPONSE_TYPE = new TypeToken<List<NewSceneResponse>>() {
}.getType();
private final Logger logger = LoggerFactory.getLogger(GroupActions.class);
private final Gson gson = new Gson();
private @Nullable GroupThingHandler handler;
@RuleAction(label = "@text/action.create-scene.label", description = "@text/action.create-scene.description")
public @ActionOutput(name = NEW_SCENE_ID_OUTPUT, type = "java.lang.Integer") Map<String, Object> createScene(
@ActionInput(name = "name", label = "@text/action.create-scene.name.label", description = "@text/action.create-scene.name.description") @Nullable String name) {
GroupThingHandler handler = this.handler;
if (handler == null) {
logger.warn("Deconz GroupActions service ThingHandler is null!");
return Map.of();
}
if (name == null) {
logger.debug("Skipping scene creation due to missing scene name");
return Map.of();
}
CompletableFuture<String> newSceneId = new CompletableFuture<>();
handler.doNetwork(Map.of("name", name), "scenes", HttpMethod.POST, newSceneId::complete);
try {
String returnedJson = newSceneId.get(2000, TimeUnit.MILLISECONDS);
List<NewSceneResponse> newSceneResponses = gson.fromJson(returnedJson, NEW_SCENE_RESPONSE_TYPE);
if (newSceneResponses != null && !newSceneResponses.isEmpty()) {
return Map.of(NEW_SCENE_ID_OUTPUT, newSceneResponses.get(0).success.id);
}
throw new IllegalStateException("response is empty");
} catch (InterruptedException | ExecutionException | TimeoutException | JsonParseException
| IllegalStateException e) {
logger.warn("Couldn't get newSceneId", e);
return Map.of();
}
}
public static Map<String, Object> createScene(ThingActions actions, @Nullable String name) {
if (actions instanceof GroupActions groupActions) {
return groupActions.createScene(name);
}
return Map.of();
}
@RuleAction(label = "@text/action.delete-scene.label", description = "@text/action.delete-scene.description")
public void deleteScene(
@ActionInput(name = "sceneId", label = "@text/action.delete-scene.sceneId.label", description = "@text/action.delete-scene.sceneId.description") @Nullable Integer sceneId) {
GroupThingHandler handler = this.handler;
if (handler == null) {
logger.warn("Deconz GroupActions service ThingHandler is null!");
return;
}
if (sceneId == null) {
logger.warn("Skipping scene deletion due to missing scene id");
return;
}
handler.doNetwork(null, "scenes/" + sceneId, HttpMethod.DELETE, null);
}
public static void deleteScene(ThingActions actions, @Nullable Integer sceneId) {
if (actions instanceof GroupActions groupActions) {
groupActions.deleteScene(sceneId);
}
}
@RuleAction(label = "@text/action.store-as-scene.label", description = "@text/action.store-as-scene.description")
public void storeScene(
@ActionInput(name = "sceneId", label = "@text/action.store-as-scene.sceneId.label", description = "@text/action.store-as-scene.sceneId.description") @Nullable Integer sceneId) {
GroupThingHandler handler = this.handler;
if (handler == null) {
logger.warn("Deconz GroupActions service ThingHandler is null!");
return;
}
if (sceneId == null) {
logger.warn("Skipping scene storage due to missing scene id");
return;
}
handler.doNetwork(null, "scenes/" + sceneId + "/store", HttpMethod.PUT, null);
}
public static void storeScene(ThingActions actions, @Nullable Integer sceneId) {
if (actions instanceof GroupActions groupActions) {
groupActions.storeScene(sceneId);
}
}
@Override
public void setThingHandler(@Nullable ThingHandler handler) {
if (handler instanceof GroupThingHandler) {
this.handler = (GroupThingHandler) handler;
}
}
@Override
public @Nullable ThingHandler getThingHandler() {
return handler;
}
}

View File

@ -58,7 +58,7 @@ public class BridgeDiscoveryParticipant implements UpnpDiscoveryParticipant {
return null;
}
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();

View File

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

View File

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

View File

@ -38,6 +38,23 @@ public class GroupAction {
public @Nullable Integer colorloopspeed;
public @Nullable Integer transitiontime;
/**
* clear this group action
*/
public void clear() {
on = null;
bri = null;
alert = null;
colormode = null;
effect = null;
hue = null;
sat = null;
ct = null;
xy = null;
}
@Override
public String toString() {
return "GroupAction{on=" + on + ", toggle=" + toggle + ", bri=" + bri + ", hue=" + hue + ", sat=" + sat

View File

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

View File

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

View File

@ -0,0 +1,29 @@
/**
* Copyright (c) 2010-2023 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.deconz.internal.dto;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link NewSceneResponse} is the response after a successful scene creation
*
* @author Jan N. Klug - Initial contribution
*/
@NonNullByDefault
public class NewSceneResponse {
public Success success = new Success();
public static class Success {
public int id = 0;
}
}

View File

@ -35,10 +35,13 @@ public class SensorConfig {
public @Nullable Integer heatsetpoint;
public @Nullable 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 + "}";
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -13,14 +13,19 @@
package org.openhab.binding.deconz.internal.handler;
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);
}
}

View File

@ -41,18 +41,20 @@ import org.openhab.core.library.types.StopMoveType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.library.types.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)));
}
}
}

View File

@ -17,28 +17,20 @@ import static org.openhab.binding.deconz.internal.BindingConstants.*;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import javax.measure.Unit;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.deconz.internal.Util;
import org.openhab.binding.deconz.internal.dto.DeconzBaseMessage;
import org.openhab.binding.deconz.internal.dto.SensorConfig;
import org.openhab.binding.deconz.internal.dto.SensorMessage;
import org.openhab.binding.deconz.internal.dto.SensorState;
import org.openhab.binding.deconz.internal.types.ResourceType;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.binding.builder.ThingBuilder;
import org.openhab.core.thing.type.ChannelKind;
import org.openhab.core.types.Command;
import org.slf4j.Logger;
@ -73,27 +65,16 @@ public abstract class SensorBaseThingHandler extends DeconzBaseThingHandler {
* Prevent a dispose/init cycle while this flag is set. Use for property updates
*/
private boolean ignoreConfigurationUpdate;
private @Nullable ScheduledFuture<?> lastSeenPollingJob;
public SensorBaseThingHandler(Thing thing, Gson gson) {
super(thing, gson, ResourceType.SENSORS);
}
@Override
public void dispose() {
ScheduledFuture<?> lastSeenPollingJob = this.lastSeenPollingJob;
if (lastSeenPollingJob != null) {
lastSeenPollingJob.cancel(true);
this.lastSeenPollingJob = null;
}
super.dispose();
}
@Override
public abstract void handleCommand(ChannelUID channelUID, Command command);
protected abstract void createTypeSpecificChannels(SensorConfig sensorState, SensorState sensorConfig);
protected abstract boolean createTypeSpecificChannels(ThingBuilder thingBuilder, SensorConfig sensorState,
SensorState sensorConfig);
protected abstract List<String> getConfigChannels();
@ -106,11 +87,10 @@ public abstract class SensorBaseThingHandler extends DeconzBaseThingHandler {
@Override
protected void processStateResponse(DeconzBaseMessage stateResponse) {
if (!(stateResponse instanceof SensorMessage)) {
if (!(stateResponse instanceof SensorMessage sensorMessage)) {
return;
}
SensorMessage sensorMessage = (SensorMessage) stateResponse;
sensorConfig = Objects.requireNonNullElse(sensorMessage.config, new SensorConfig());
sensorState = Objects.requireNonNullElse(sensorMessage.state, new SensorState());
@ -133,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));
}
}

View File

@ -33,15 +33,18 @@ import org.openhab.binding.deconz.internal.dto.SensorState;
import org.openhab.binding.deconz.internal.dto.ThermostatUpdateConfig;
import org.openhab.binding.deconz.internal.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);

View File

@ -25,7 +25,6 @@ import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.deconz.internal.dto.SensorConfig;
import org.openhab.binding.deconz.internal.dto.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

View File

@ -48,7 +48,7 @@ public class AsyncHttpClient {
* @param timeout A timeout
* @return The result
*/
public CompletableFuture<Result> post(String address, String jsonString, int timeout) {
public CompletableFuture<Result> post(String address, @Nullable String jsonString, int timeout) {
return doNetwork(HttpMethod.POST, address, jsonString, timeout);
}
@ -101,15 +101,16 @@ public class AsyncHttpClient {
}
request.method(method).timeout(timeout, TimeUnit.MILLISECONDS).send(new BufferingResponseListener() {
@NonNullByDefault({})
@Override
public void onComplete(org.eclipse.jetty.client.api.Result result) {
public void onComplete(@NonNullByDefault({}) org.eclipse.jetty.client.api.Result result) {
final HttpResponse response = (HttpResponse) result.getResponse();
if (result.getFailure() != null) {
f.completeExceptionally(result.getFailure());
return;
}
f.complete(new Result(getContentAsString(), response.getStatus()));
String content = getContentAsString();
f.complete(new Result(content != null ? content : "", response.getStatus()));
}
});
return f;

View File

@ -16,6 +16,9 @@ import java.net.URI;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import org.eclipse.jdt.annotation.NonNullByDefault;
@ -29,6 +32,7 @@ import org.eclipse.jetty.websocket.api.annotations.WebSocket;
import org.eclipse.jetty.websocket.client.WebSocketClient;
import org.openhab.binding.deconz.internal.dto.DeconzBaseMessage;
import org.openhab.binding.deconz.internal.types.ResourceType;
import org.openhab.core.common.ThreadPoolManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -46,23 +50,33 @@ import com.google.gson.Gson;
public class WebSocketConnection {
private static final AtomicInteger INSTANCE_COUNTER = new AtomicInteger();
private final Logger logger = LoggerFactory.getLogger(WebSocketConnection.class);
private final ScheduledExecutorService scheduler = ThreadPoolManager.getScheduledPool("thingHandler");
private final WebSocketClient client;
private final String socketName;
private final Gson gson;
private int watchdogInterval;
private final WebSocketConnectionListener connectionListener;
private final Map<String, WebSocketMessageListener> listeners = new ConcurrentHashMap<>();
private ConnectionState connectionState = ConnectionState.DISCONNECTED;
private @Nullable ScheduledFuture<?> watchdogJob;
private @Nullable Session session;
public WebSocketConnection(WebSocketConnectionListener listener, WebSocketClient client, Gson gson) {
public WebSocketConnection(WebSocketConnectionListener listener, WebSocketClient client, Gson gson,
int watchdogInterval) {
this.connectionListener = listener;
this.client = client;
this.client.setMaxIdleTimeout(0);
this.gson = gson;
this.socketName = "Websocket$" + System.currentTimeMillis() + "-" + INSTANCE_COUNTER.incrementAndGet();
this.watchdogInterval = watchdogInterval;
}
public void setWatchdogInterval(int watchdogInterval) {
this.watchdogInterval = watchdogInterval;
}
public void start(String ip) {
@ -73,18 +87,47 @@ public class WebSocketConnection {
return;
} else if (connectionState == ConnectionState.DISCONNECTING) {
logger.warn("{} trying to re-connect while still disconnecting", socketName);
return;
}
try {
connectionState = ConnectionState.CONNECTING;
URI destUri = URI.create("ws://" + ip);
client.start();
logger.debug("Trying to connect {} to {}", socketName, destUri);
client.connect(this, destUri).get();
} catch (Exception e) {
connectionListener.connectionLost("Error while connecting: " + e.getMessage());
String reason = "Error while connecting: " + e.getMessage();
if (e.getMessage() == null) {
logger.warn("{}: {}", socketName, reason, e);
} else {
logger.warn("{}: {}", socketName, reason);
}
connectionListener.webSocketConnectionLost(reason);
}
}
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

View File

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

View File

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

View File

@ -21,20 +21,24 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Type of a group as reported by the REST API for usage in {@link org.openhab.binding.deconz.internal.dto.LightMessage}
* Type of a group as reported by the REST API for usage in
* {@link org.openhab.binding.deconz.internal.dto.LightMessage}
*
* @author Jan N. Klug - Initial contribution
*/
@NonNullByDefault
public enum GroupType {
LIGHT_GROUP("LightGroup"),
LUMINAIRE("Luminaire"),
ROOM("Room"),
LIGHT_SOURCE("Lightsource"),
UNKNOWN("");
private static final Map<String, GroupType> MAPPING = Arrays.stream(GroupType.values())
.collect(Collectors.toMap(v -> v.type, v -> v));
private static final Logger LOGGER = LoggerFactory.getLogger(GroupType.class);
private String type;
private final String type;
GroupType(String type) {
this.type = type;

View File

@ -21,7 +21,8 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Type of a light as reported by the REST API for usage in {@link org.openhab.binding.deconz.internal.dto.LightMessage}
* Type of a light as reported by the REST API for usage in
* {@link org.openhab.binding.deconz.internal.dto.LightMessage}
*
* @author Jan N. Klug - Initial contribution
*/
@ -46,7 +47,7 @@ public enum LightType {
.collect(Collectors.toMap(v -> v.type, v -> v));
private static final Logger LOGGER = LoggerFactory.getLogger(LightType.class);
private String type;
private final String type;
LightType(String type) {
this.type = type;

View File

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

View File

@ -21,7 +21,8 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Thermostat mode as reported by the REST API for usage in {@link org.openhab.binding.deconz.internal.dto.SensorConfig}
* Thermostat mode as reported by the REST API for usage in
* {@link org.openhab.binding.deconz.internal.dto.SensorConfig}
*
* @author Lukas Agethen - Initial contribution
*/
@ -36,7 +37,7 @@ public enum ThermostatMode {
.collect(Collectors.toMap(v -> v.deconzValue, v -> v));
private static final Logger LOGGER = LoggerFactory.getLogger(ThermostatMode.class);
private String deconzValue;
private final String deconzValue;
ThermostatMode(String deconzValue) {
this.deconzValue = deconzValue;

View File

@ -5,33 +5,68 @@
xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0 https://openhab.org/schemas/config-description-1.0.0.xsd">
<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>

View File

@ -5,59 +5,42 @@ addon.deconz.description = Allows to use the real-time channel of the deCONZ sof
# thing types
thing-type.deconz.airqualitysensor.label = Carbon-monoxide Sensor
thing-type.deconz.alarmsensor.label = Alarm Sensor
thing-type.deconz.alarmsensor.description = An alarm sensor
thing-type.deconz.batterysensor.label = Battery Sensor
thing-type.deconz.batterysensor.description = A battery sensor
thing-type.deconz.carbonmonoxidesensor.label = Carbon-monoxide Sensor
thing-type.deconz.airqualitysensor.label = Air quality Sensor
thing-type.deconz.airqualitysensor.description = An air quality sensor
thing-type.deconz.colorcontrol.label = Color Controller
thing-type.deconz.colorlight.label = Color Light
thing-type.deconz.colorlight.description = A dimmable light with adjustable color.
thing-type.deconz.colortemperaturelight.label = Color-Temperature Light
thing-type.deconz.colortemperaturelight.description = A dimmable light with adjustable color temperature.
thing-type.deconz.consumptionsensor.label = Consumption Sensor
thing-type.deconz.consumptionsensor.description = A consumption sensor
thing-type.deconz.daylightsensor.label = Daylight Sensor
thing-type.deconz.daylightsensor.description = A daylight sensor
thing-type.deconz.deconz.label = deCONZ
thing-type.deconz.deconz.description = A running deCONZ software instance.
thing-type.deconz.dimmablelight.label = Dimmable Light
thing-type.deconz.dimmablelight.description = A dimmable light.
thing-type.deconz.doorlock.label = Doorlock
thing-type.deconz.doorlock.description = A doorlock that can be locked (ON) or unlocked (OFF).
thing-type.deconz.extendedcolorlight.label = Color Light
thing-type.deconz.extendedcolorlight.description = A dimmable light with adjustable color.
thing-type.deconz.firesensor.label = Fire Sensor
thing-type.deconz.firesensor.description = A fire sensor
thing-type.deconz.humiditysensor.label = Humidity Sensor
thing-type.deconz.humiditysensor.description = A humidity sensor
thing-type.deconz.lightgroup.label = Light Group
thing-type.deconz.lightsensor.label = Light Sensor
thing-type.deconz.lightsensor.description = A light sensor
thing-type.deconz.moisturesensor.label = Moisture Sensor
thing-type.deconz.onofflight.label = On/Off Light
thing-type.deconz.onofflight.description = A light that can be turned on or off.
thing-type.deconz.openclosesensor.label = Open/Close Sensor
thing-type.deconz.openclosesensor.description = An open/close sensor
thing-type.deconz.powersensor.label = Power Sensor
thing-type.deconz.powersensor.description = A power sensor
thing-type.deconz.presencesensor.label = Presence Sensor
thing-type.deconz.presencesensor.description = A Presence sensor
thing-type.deconz.pressuresensor.label = Pressure Sensor
thing-type.deconz.pressuresensor.description = A pressure senor
thing-type.deconz.switch.label = Switch/Button
thing-type.deconz.switch.description = A switch or button
thing-type.deconz.temperaturesensor.label = Temperature Sensor
thing-type.deconz.temperaturesensor.description = A temperature sensor
thing-type.deconz.thermostat.label = Thermostat
thing-type.deconz.thermostat.description = A Thermostat sensor/actor
thing-type.deconz.vibrationsensor.label = Vibration Sensor
thing-type.deconz.vibrationsensor.description = A vibration sensor
thing-type.deconz.warningdevice.label = Warning Device
thing-type.deconz.warningdevice.description = A warning device
thing-type.deconz.waterleakagesensor.label = Water Leakage Sensor
thing-type.deconz.waterleakagesensor.description = A water leakage sensor
thing-type.deconz.windowcovering.label = Window Covering
thing-type.deconz.windowcovering.description = A device to cover windows.
@ -65,24 +48,32 @@ thing-type.deconz.windowcovering.description = A device to cover windows.
thing-type.config.deconz.bridge.apikey.label = API Key
thing-type.config.deconz.bridge.apikey.description = If no API Key is provided, a new one will be requested. You need to authorize the access on the deCONZ web interface.
thing-type.config.deconz.bridge.group.http.label = HTTP Connection
thing-type.config.deconz.bridge.group.websocket.label = Websocket Connection
thing-type.config.deconz.bridge.host.label = Host Address
thing-type.config.deconz.bridge.host.description = IP address or host name of deCONZ interface.
thing-type.config.deconz.bridge.httpPort.label = HTTP Port
thing-type.config.deconz.bridge.httpPort.label = Port
thing-type.config.deconz.bridge.httpPort.description = Port of the deCONZ HTTP interface.
thing-type.config.deconz.bridge.port.label = Websocket Port
thing-type.config.deconz.bridge.port.label = Port
thing-type.config.deconz.bridge.port.description = Port of the deCONZ Websocket.
thing-type.config.deconz.bridge.timeout.label = Timeout
thing-type.config.deconz.bridge.timeout.description = Timeout for asynchronous HTTP requests (in milliseconds).
thing-type.config.deconz.bridge.websocketTimeout.label = Timeout
thing-type.config.deconz.bridge.websocketTimeout.description = Timeout for the websocket connection (in seconds).
thing-type.config.deconz.colorlight.colormode.label = Color Mode
thing-type.config.deconz.colorlight.colormode.description = Override the default color mode (auto-detect)
thing-type.config.deconz.colorlight.colormode.option.hs = HSB
thing-type.config.deconz.colorlight.colormode.option.xy = XY
thing-type.config.deconz.colorlight.id.label = Device ID
thing-type.config.deconz.colorlight.id.description = The deCONZ bridge assigns an integer number ID to each device.
thing-type.config.deconz.colorlight.lastSeenPolling.label = LastSeen Poll Interval
thing-type.config.deconz.colorlight.lastSeenPolling.description = Interval to poll the deCONZ Gateway for this light's "last_seen" channel. Polling is disabled when set to 0 (default: 1440, once per day).
thing-type.config.deconz.colorlight.transitiontime.label = Transition Time
thing-type.config.deconz.colorlight.transitiontime.description = Time to move between two states. If empty, the default of the device is used. Resolution is 1/10 second.
thing-type.config.deconz.light.id.label = Device ID
thing-type.config.deconz.light.id.description = The deCONZ bridge assigns an integer number ID to each device.
thing-type.config.deconz.light.lastSeenPolling.label = LastSeen Poll Interval
thing-type.config.deconz.light.lastSeenPolling.description = Interval to poll the deCONZ Gateway for this light's "last_seen" channel. Polling is disabled when set to 0 (default: 1440, once per day).
thing-type.config.deconz.light.transitiontime.label = Transition Time
thing-type.config.deconz.light.transitiontime.description = Time to move between two states. If empty, the default of the device is used. Resolution is 1/10 second.
thing-type.config.deconz.lightgroup.colormode.label = Color Mode
@ -91,6 +82,8 @@ thing-type.config.deconz.lightgroup.colormode.option.hs = HSB
thing-type.config.deconz.lightgroup.colormode.option.xy = XY
thing-type.config.deconz.lightgroup.id.label = Device ID
thing-type.config.deconz.lightgroup.id.description = The deCONZ bridge assigns an integer number ID to each group.
thing-type.config.deconz.lightgroup.transitiontime.label = Transition Time
thing-type.config.deconz.lightgroup.transitiontime.description = Time to move between two states. If empty, the default of the group is used. Resolution is 1/10 second.
thing-type.config.deconz.sensor.id.label = Device ID
thing-type.config.deconz.sensor.id.description = The deCONZ bridge assigns an integer number ID to each device.
thing-type.config.deconz.sensor.lastSeenPolling.label = LastSeen Poll Interval
@ -98,6 +91,10 @@ thing-type.config.deconz.sensor.lastSeenPolling.description = Interval to poll t
# channel types
channel-type.deconz.airquality.label = Air Quality
channel-type.deconz.airquality.description = Current air quality level based on volatile organic compounds (VOCs) measurement. Example: good or poor, ...
channel-type.deconz.airqualityppb.label = Air Quality (ppb)
channel-type.deconz.airqualityppb.description = Current air quality based on measurements of volatile organic compounds (VOCs). The measured value is specified in ppb (parts per billion).
channel-type.deconz.alarm.label = Alarm
channel-type.deconz.alarm.description = Alarm was triggered.
channel-type.deconz.alert.label = Alert
@ -114,10 +111,6 @@ channel-type.deconz.buttonevent.label = Button Trigger
channel-type.deconz.buttonevent.description = This channel is triggered on a button event. The trigger payload consists of the button event number.
channel-type.deconz.carbonmonoxide.label = Carbon-monoxide
channel-type.deconz.carbonmonoxide.description = Carbon-monoxide was detected.
channel-type.deconz.airquality.label = Air quality level
channel-type.deconz.airquality.description = Current air quality level based on volatile organic compounds (VOCs) measurement. Example: good or poor, ...
channel-type.deconz.airqualityppb.label = Air quality in ppb
channel-type.deconz.airqualityppb.description = Current air quality based on measurements of volatile organic compounds (VOCs). The measured value is specified in ppb (parts per billion).
channel-type.deconz.consumption.label = Consumption
channel-type.deconz.consumption.description = Current consumption
channel-type.deconz.ct.label = Color Temperature
@ -130,6 +123,7 @@ channel-type.deconz.daylight.label = Daylight
channel-type.deconz.daylight.description = Light level is above the daylight threshold.
channel-type.deconz.effect.label = Effect Channel
channel-type.deconz.effectSpeed.label = Effect Speed Channel
channel-type.deconz.externalwindowopen.label = External Window Open
channel-type.deconz.fire.label = Fire
channel-type.deconz.fire.description = A fire was detected.
channel-type.deconz.gesture.label = Gesture
@ -145,7 +139,7 @@ channel-type.deconz.gesture.state.option.7 = Rotate Clockwise
channel-type.deconz.gesture.state.option.8 = Rotate Counter Clockwise
channel-type.deconz.gestureevent.label = Gesture Trigger
channel-type.deconz.gestureevent.description = This channel is triggered on a gesture event. The trigger payload consists of the gesture event number.
channel-type.deconz.heatsetpoint.label = Target Temperature
channel-type.deconz.heatsetpoint.label = Target temperature
channel-type.deconz.heatsetpoint.description = Target temperature
channel-type.deconz.humidity.label = Humidity
channel-type.deconz.humidity.description = Current humidity
@ -156,7 +150,6 @@ channel-type.deconz.last_updated.label = Last Updated
channel-type.deconz.last_updated.description = The date and time when the sensor was last updated.
channel-type.deconz.last_updated.state.pattern = %1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS
channel-type.deconz.light.label = Lightlevel
channel-type.deconz.light.description = A light level
channel-type.deconz.light.state.option.daylight = Daylight
channel-type.deconz.light.state.option.sunset = Sunset
channel-type.deconz.light.state.option.dark = Dark
@ -165,11 +158,15 @@ channel-type.deconz.light_level.description = Current light level.
channel-type.deconz.lightlux.label = Illuminance
channel-type.deconz.lightlux.description = Current light illuminance
channel-type.deconz.lock.label = Lock
channel-type.deconz.locked.label = Locked
channel-type.deconz.locked.description = Status of this thermostat's child lock.
channel-type.deconz.mode.label = Mode
channel-type.deconz.mode.description = Current mode
channel-type.deconz.mode.state.option.AUTO = auto
channel-type.deconz.mode.state.option.HEAT = heat
channel-type.deconz.mode.state.option.OFF = off
channel-type.deconz.moisture.label = Moisture
channel-type.deconz.moisture.description = Current moisture
channel-type.deconz.offset.label = Offset
channel-type.deconz.offset.description = Temperature offset
channel-type.deconz.ontime.label = On Time
@ -196,8 +193,28 @@ channel-type.deconz.voltage.label = Voltage
channel-type.deconz.voltage.description = Current voltage
channel-type.deconz.waterleakage.label = Water Leakage
channel-type.deconz.waterleakage.description = Water leakage detected
channel-type.deconz.windowopen.label = Window Open
# thing status descriptions
offline.light-not-reachable = Not reachable
offline.sensor-not-reachable = Not reachable
# actions
action.permit-join-network.duration.label = Duration
action.permit-join-network.duration.description = Number of seconds to allow new devices to join.
action.permit-join-network.label = permit join Zigbee network
action.permit-join-network.description = Permits new devices to join the Zigbee network for a given duration (default 120s).
action.create-scene.label = create a scene
action.create-scene.description = Creates a new scene and returns the new scene's id.
action.create-scene.name.label = Name
action.create-scene.name.description = Name of the scene to create.
action.delete-scene.label = delete a scene
action.delete-scene.description = Deletes a scene.
action.delete-scene.sceneId.label = Scene id
action.delete-scene.sceneId.description = Id of the scene to delete.
action.store-as-scene.label = store as scene
action.store-as-scene.description = Stores the current light state as scene
action.store-as-scene.sceneId.label = Scene id
action.store-as-scene.sceneId.description = Id of the scene to store current group's state as.

View File

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

View File

@ -9,10 +9,9 @@
<bridge-type-ref id="deconz"/>
</supported-bridge-type-refs>
<label>Warning Device</label>
<description>A warning device</description>
<category>Siren</category>
<channels>
<channel id="alert" typeId="alert"></channel>
<channel typeId="alert" id="alert"/>
</channels>
<representation-property>uid</representation-property>
@ -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>

View File

@ -9,13 +9,16 @@
<bridge-type-ref id="deconz"/>
</supported-bridge-type-refs>
<label>Presence Sensor</label>
<description>A Presence sensor</description>
<channels>
<channel typeId="system.motion" id="presence"/>
<channel typeId="last_updated" id="last_updated"/>
<channel typeId="system.power" id="enabled"/>
</channels>
<properties>
<property name="thingTypeVersion">1</property>
</properties>
<representation-property>uid</representation-property>
<config-description-ref uri="thing-type:deconz:sensor"/>
@ -42,7 +45,6 @@
<bridge-type-ref id="deconz"/>
</supported-bridge-type-refs>
<label>Power Sensor</label>
<description>A power sensor</description>
<channels>
<channel typeId="power" id="power"/>
<channel typeId="last_updated" id="last_updated"/>
@ -58,7 +60,7 @@
<label>Power</label>
<description>Current power usage</description>
<category>Energy</category>
<state readOnly="true" pattern="%.1f %unit%"></state>
<state readOnly="true" pattern="%.1f %unit%"/>
</channel-type>
<channel-type id="voltage">
@ -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>

View File

@ -0,0 +1,81 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
<update:update-descriptions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:update="https://openhab.org/schemas/update-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/update-description/v1.0.0 https://openhab.org/schemas/update-description-1.0.0.xsd">
<thing-type uid="deconz:batterysensor">
<instruction-set targetVersion="1">
<update-channel id="battery_level">
<type>system:battery-level</type>
</update-channel>
</instruction-set>
</thing-type>
<thing-type uid="deconz:colorcontrol">
<instruction-set targetVersion="1">
<update-channel id="color">
<type>system:color</type>
</update-channel>
</instruction-set>
</thing-type>
<thing-type uid="deconz:colorlight">
<instruction-set targetVersion="1">
<update-channel id="color">
<type>system:color</type>
</update-channel>
</instruction-set>
</thing-type>
<thing-type uid="deconz:colortemperaturelight">
<instruction-set targetVersion="1">
<update-channel id="brightness">
<type>system:brightness</type>
</update-channel>
</instruction-set>
</thing-type>
<thing-type uid="deconz:dimmablelight">
<instruction-set targetVersion="1">
<update-channel id="brightness">
<type>system:brightness</type>
</update-channel>
</instruction-set>
</thing-type>
<thing-type uid="deconz:extendedcolorlight">
<instruction-set targetVersion="1">
<update-channel id="color">
<type>system:color</type>
</update-channel>
</instruction-set>
</thing-type>
<thing-type uid="deconz:lightgroup">
<instruction-set targetVersion="1">
<update-channel id="color">
<type>system:color</type>
</update-channel>
</instruction-set>
</thing-type>
<thing-type uid="deconz:onofflight">
<instruction-set targetVersion="1">
<update-channel id="switch">
<type>system:power</type>
</update-channel>
</instruction-set>
</thing-type>
<thing-type uid="deconz:presencesensor">
<instruction-set targetVersion="1">
<update-channel id="enabled">
<type>system:power</type>
</update-channel>
<update-channel id="presence">
<type>system:motion</type>
</update-channel>
</instruction-set>
</thing-type>
</update:update-descriptions>

View File

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

View File

@ -0,0 +1,89 @@
/**
* Copyright (c) 2010-2023 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.deconz;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.mockito.ArgumentMatchers.eq;
import static org.openhab.binding.deconz.internal.BindingConstants.CHANNEL_ALL_ON;
import static org.openhab.binding.deconz.internal.BindingConstants.CHANNEL_ANY_ON;
import static org.openhab.binding.deconz.internal.BindingConstants.THING_TYPE_LIGHTGROUP;
import static org.openhab.core.thing.internal.ThingManagerImpl.PROPERTY_THING_TYPE_VERSION;
import java.io.IOException;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
import org.openhab.binding.deconz.internal.DeconzDynamicCommandDescriptionProvider;
import org.openhab.binding.deconz.internal.dto.GroupMessage;
import org.openhab.binding.deconz.internal.handler.GroupThingHandler;
import org.openhab.binding.deconz.internal.types.GroupType;
import org.openhab.binding.deconz.internal.types.GroupTypeDeserializer;
import org.openhab.core.library.CoreItemFactory;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.thing.binding.ThingHandlerCallback;
import org.openhab.core.thing.binding.builder.ChannelBuilder;
import org.openhab.core.thing.binding.builder.ThingBuilder;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
/**
* This class provides tests for deconz light groups
*
* @author Christoph Weitkamp - Initial contribution
*/
@ExtendWith(MockitoExtension.class)
@NonNullByDefault
public class LightGroupTest {
private @NonNullByDefault({}) Gson gson;
private @Mock @NonNullByDefault({}) ThingHandlerCallback thingHandlerCallback;
private @Mock @NonNullByDefault({}) DeconzDynamicCommandDescriptionProvider commandDescriptionProvider;
@BeforeEach
public void initialize() {
GsonBuilder gsonBuilder = new GsonBuilder();
gsonBuilder.registerTypeAdapter(GroupType.class, new GroupTypeDeserializer());
gson = gsonBuilder.create();
}
@Test
public void lightGroupUpdateTest() throws IOException {
GroupMessage lightMessage = DeconzTest.getObjectFromJson("group.json", GroupMessage.class, gson);
assertNotNull(lightMessage);
ThingUID thingUID = new ThingUID("deconz", "lightgroup");
ChannelUID channelUIDAllOn = new ChannelUID(thingUID, CHANNEL_ALL_ON);
ChannelUID channelUIDAnyOn = new ChannelUID(thingUID, CHANNEL_ANY_ON);
Thing group = ThingBuilder.create(THING_TYPE_LIGHTGROUP, thingUID)
.withProperties(Map.of(PROPERTY_THING_TYPE_VERSION, "1"))
.withChannel(ChannelBuilder.create(channelUIDAllOn, CoreItemFactory.SWITCH).build())
.withChannel(ChannelBuilder.create(channelUIDAnyOn, CoreItemFactory.SWITCH).build()).build();
GroupThingHandler groupThingHandler = new GroupThingHandler(group, gson, commandDescriptionProvider);
groupThingHandler.setCallback(thingHandlerCallback);
groupThingHandler.messageReceived(lightMessage);
Mockito.verify(thingHandlerCallback).stateUpdated(eq(channelUIDAllOn), eq(OnOffType.OFF));
Mockito.verify(thingHandlerCallback).stateUpdated(eq(channelUIDAnyOn), eq(OnOffType.OFF));
}
}

View File

@ -15,6 +15,7 @@ package org.openhab.binding.deconz;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.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")));
}
}

View File

@ -44,6 +44,7 @@ import org.openhab.core.thing.ThingUID;
import org.openhab.core.thing.binding.ThingHandlerCallback;
import org.openhab.core.thing.binding.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)));

View File

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

View File

@ -0,0 +1,30 @@
{
"action": {
"alert": "none",
"bri": 127,
"colormode": "hs",
"ct": 0,
"effect": "none",
"hue": 0,
"on": false,
"sat": 127,
"scene": null,
"xy": [
0,
0
]
},
"devicemembership": [
"3"
],
"etag": "586d2448a818aa7f6f3baa4907f43468",
"id": "1",
"lights": [],
"name": "RM01",
"scenes": [],
"state": {
"all_on": false,
"any_on": false
},
"type": "LightGroup"
}

View File

@ -0,0 +1,27 @@
{
"config": {
"battery": 85,
"displayflipped": null,
"heatsetpoint": 2500,
"locked": null,
"mode": "auto",
"offset": 0,
"on": true,
"reachable": true
},
"ep": 1,
"etag": "717549a99371f3ea1a5f0b40f1537094",
"lastseen": "2020-05-31T20:24:55.819",
"manufacturername": "Eurotronic",
"modelid": "SPZB0001",
"name": "Test Thermostat",
"state": {
"lastupdated": "2020-05-31T20:24:55.819",
"on": true,
"temperature": 1650,
"valve": 255
},
"swversion": "20191014",
"type": "ZHAThermostat",
"uniqueid": "00:15:8d:00:01:ff:8a:00-01-0201"
}

View File

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