/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
There is one bridge (`deconz`) that manages the connection to the deCONZ software instance.
These sensors are supported:
-| Device type | Resource Type | Thing type |
-|-----------------------------------|-----------------------------------|----------------------|
-| Presence Sensor | ZHAPresence, CLIPPresence | `presencesensor` |
-| Power Sensor | ZHAPower, CLIPPower | `powersensor` |
-| Consumption Sensor | ZHAConsumption | `consumptionsensor` |
-| Switch | ZHASwitch | `switch` |
-| Light Sensor | ZHALightLevel | `lightsensor` |
-| Temperature Sensor | ZHATemperature | `temperaturesensor` |
-| Humidity Sensor | ZHAHumidity | `humiditysensor` |
-| Pressure Sensor | ZHAPressure | `pressuresensor` |
-| Open/Close Sensor | ZHAOpenClose | `openclosesensor` |
-| Water Leakage Sensor | ZHAWater | `waterleakagesensor` |
-| Alarm Sensor | ZHAAlarm | `alarmsensor` |
-| Fire Sensor | ZHAFire | `firesensor` |
-| Vibration Sensor | ZHAVibration | `vibrationsensor` |
-| deCONZ Artificial Daylight Sensor | deCONZ specific: simulated sensor | `daylightsensor` |
-| Carbon-Monoxide Sensor | ZHACarbonmonoxide | `carbonmonoxide` |
-| Air quality Sensor | ZHAAirQuality | `airqualitysensor` |
-| Color Controller | ZBT-Remote-ALL-RGBW | `colorcontrol` |
-
-Additionally lights, window coverings (blinds), door locks and thermostats are supported:
+| Device type | Resource Type | Thing type |
+|-----------------------------------|-----------------------------------|------------------------|
+| Presence Sensor | ZHAPresence, CLIPPresence | `presencesensor` |
+| Power Sensor | ZHAPower, CLIPPower | `powersensor` |
+| Consumption Sensor | ZHAConsumption | `consumptionsensor` |
+| Switch | ZHASwitch | `switch` |
+| Light Sensor | ZHALightLevel | `lightsensor` |
+| Temperature Sensor | ZHATemperature | `temperaturesensor` |
+| Humidity Sensor | ZHAHumidity | `humiditysensor` |
+| Pressure Sensor | ZHAPressure | `pressuresensor` |
+| Open/Close Sensor | ZHAOpenClose | `openclosesensor` |
+| Water Leakage Sensor | ZHAWater | `waterleakagesensor` |
+| Alarm Sensor | ZHAAlarm | `alarmsensor` |
+| Fire Sensor | ZHAFire | `firesensor` |
+| Vibration Sensor | ZHAVibration | `vibrationsensor` |
+| deCONZ Artificial Daylight Sensor | deCONZ specific: simulated sensor | `daylightsensor` |
+| Carbon-Monoxide Sensor | ZHACarbonmonoxide | `carbonmonoxidesensor` |
+| Airquality Sensor | ZHAAirquality | `airqualitysensor` |
+| Moisture Sensor | ZHAMoisture | `moisturesensor` |
+| Color Controller | ZBT-Remote-ALL-RGBW | `colorcontrol` |
+
+Additionally, lights, window coverings (blinds), door locks and thermostats are supported:
| Device type | Resource Type | Thing type |
|--------------------------------------|-----------------------------------------------|-------------------------|
| 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
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.
The sensor devices support some of the following channels:
-| Channel Type ID | Item Type | Access Mode | Description | Thing types |
-|-----------------|--------------------------|-------------|-------------------------------------------------------------------------------------------|---------------------------------------------------|
-| presence | Switch | R | Status of presence: `ON` = presence; `OFF` = no-presence | presencesensor |
-| enabled | Switch | R/W | This channel activates or deactivates the sensor | presencesensor |
-| last_updated | DateTime | R | Timestamp when the sensor was last updated | all, except daylightsensor |
-| last_seen | DateTime | R | Timestamp when the sensor was last seen | all, except daylightsensor |
-| power | Number:Power | R | Current power usage in Watts | powersensor, sometimes for consumptionsensor |
-| consumption | Number:Energy | R | Current power usage in Watts/Hour | consumptionsensor |
-| voltage | Number:ElectricPotential | R | Current voltage in V | some powersensors |
-| current | Number:ElectricCurrent | R | Current current in mA | some powersensors |
-| button | Number | R | Last pressed button id on a switch | switch, colorcontrol |
-| gesture | Number | R | A gesture that was performed with the switch | switch |
-| lightlux | Number:Illuminance | R | Current light illuminance in Lux | lightsensor |
-| light_level | Number | R | Current light level | lightsensor |
-| dark | Switch | R | Light level is below the darkness threshold | lightsensor, sometimes for presencesensor |
-| daylight | Switch | R | Light level is above the daylight threshold | lightsensor |
-| temperature | Number:Temperature | R | Current temperature in ËšC | temperaturesensor, some Xiaomi sensors,thermostat |
-| humidity | Number:Dimensionless | R | Current humidity in % | humiditysensor |
-| pressure | Number:Pressure | R | Current pressure in hPa | pressuresensor |
-| open | Contact | R | Status of contacts: `OPEN`; `CLOSED` | openclosesensor |
-| waterleakage | Switch | R | Status of water leakage: `ON` = water leakage detected; `OFF` = no water leakage detected | waterleakagesensor |
-| fire | Switch | R | Status of a fire: `ON` = fire was detected; `OFF` = no fire detected | firesensor |
-| alarm | Switch | R | Status of an alarm: `ON` = alarm was triggered; `OFF` = no alarm | alarmsensor |
-| tampered | Switch | R | Status of a zone: `ON` = zone is being tampered; `OFF` = zone is not tampered | any IAS sensor |
-| vibration | Switch | R | Status of vibration: `ON` = vibration was detected; `OFF` = no vibration | alarmsensor |
-| light | String | R | Light level: `Daylight`; `Sunset`; `Dark` | daylightsensor |
-| value | Number | R | Sun position: `130` = dawn; `140` = sunrise; `190` = sunset; `210` = dusk | daylightsensor |
-| battery_level | Number | R | Battery level (in %) | any battery-powered sensor |
-| battery_low | Switch | R | Battery level low: `ON`; `OFF` | any battery-powered sensor |
-| carbonmonoxide | Switch | R | `ON` = carbon monoxide detected | carbonmonoxide |
-| airquality | String | R | Current air quality level | airqualitysensor |
-| airqualityppb | Number:Dimensionless | R | Current air quality ppb (parts per billion) | airqualitysensor |
-| color | Color | R | Color set by remote | colorcontrol |
-| windowopen | Contact | R | `windowopen` status is reported by some thermostats | thermostat |
-
-**NOTE:** Beside other non mandatory channels, the `battery_level` and `battery_low` channels will be added to the Thing during runtime if the sensor is battery-powered.
+| Channel Type ID | Item Type | Access Mode | Description | Thing types |
+|--------------------|--------------------------|-------------|-------------------------------------------------------------------------------------------|---------------------------------------------------|
+| presence | Switch | R | Status of presence: `ON` = presence; `OFF` = no-presence | presencesensor |
+| enabled | Switch | R/W | This channel activates or deactivates the sensor | presencesensor |
+| last_updated | DateTime | R | Timestamp when the sensor was last updated | all, except daylightsensor |
+| last_seen | DateTime | R | Timestamp when the sensor was last seen | all, except daylightsensor |
+| power | Number:Power | R | Power usage in Watts | powersensor, sometimes for consumptionsensor |
+| consumption | Number:Energy | R | Energy in Watt*Hour | consumptionsensor |
+| voltage | Number:ElectricPotential | R | Voltage in V | some powersensors |
+| current | Number:ElectricCurrent | R | Current in mA | some powersensors |
+| button | Number | R | Last pressed button id on a switch | switch, colorcontrol |
+| gesture | Number | R | A gesture that was performed with the switch | switch |
+| lightlux | Number:Illuminance | R | Light illuminance in Lux | lightsensor |
+| light_level | Number | R | Light level | lightsensor |
+| dark | Switch | R | Light level is below the darkness threshold | lightsensor, sometimes for presencesensor |
+| daylight | Switch | R | Light level is above the daylight threshold | lightsensor |
+| temperature | Number:Temperature | R | Temperature in ËšC | temperaturesensor, some Xiaomi sensors,thermostat |
+| humidity | Number:Dimensionless | R | Humidity in % | humiditysensor |
+| pressure | Number:Pressure | R | Pressure in hPa | pressuresensor |
+| open | Contact | R | Status of contacts: `OPEN`; `CLOSED` | openclosesensor |
+| waterleakage | Switch | R | Status of water leakage: `ON` = water leakage detected; `OFF` = no water leakage detected | waterleakagesensor |
+| fire | Switch | R | Status of a fire: `ON` = fire was detected; `OFF` = no fire detected | firesensor |
+| alarm | Switch | R | Status of an alarm: `ON` = alarm was triggered; `OFF` = no alarm | alarmsensor |
+| tampered | Switch | R | Status of a zone: `ON` = zone is being tampered; `OFF` = zone is not tampered | any IAS sensor |
+| vibration | Switch | R | Status of vibration: `ON` = vibration was detected; `OFF` = no vibration | alarmsensor |
+| light | String | R | Light level: `Daylight`; `Sunset`; `Dark` | daylightsensor |
+| value | Number | R | Sun position: `130` = dawn; `140` = sunrise; `190` = sunset; `210` = dusk | daylightsensor |
+| battery_level | Number | R | Battery level (in %) | any battery-powered sensor |
+| battery_low | Switch | R | Battery level low: `ON`; `OFF` | any battery-powered sensor |
+| carbonmonoxide | Switch | R | `ON` = carbon monoxide detected | carbonmonoxide |
+| color | Color | R | Color set by remote | colorcontrol |
+| windowopen | Contact | R | `windowopen` status is reported by some thermostats | thermostat |
+| externalwindowopen | Contact | R/W | forward a status to a theromastat (some devices) | thermostat |
+| locked | Switch | R/W | reports/sets the childlock on some thermostats | thermostat |
+| airquality | String | R | Airquality as string | airqualitysensor |
+| airqualityppb | Number:Dimensionless | R | Airquality (in parts-per-billion) | airqualitysensor |
+| moisture | Number:Dimensionless | R | Moisture | moisturesensor |
+
+**NOTE:** Beside other non-mandatory channels, the `battery_level` and `battery_low` channels will be added to the Thing during runtime if the sensor is battery-powered.
The specification of your sensor depends on the deCONZ capabilities.
Have a detailed look for [supported devices](https://github.com/dresden-elektronik/deconz-rest-plugin/wiki/Supported-Devices).
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.
| 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
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.
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
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");
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";
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";
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";
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;
@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);
/**
*
* @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));
}
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;
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.
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));
}
}
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;
}
/**
- * 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
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
}
/**
- * 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
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);
+ }
}
--- /dev/null
+/**
+ * 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;
+ }
+}
--- /dev/null
+/**
+ * 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;
+ }
+}
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();
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();
import static org.openhab.binding.deconz.internal.BindingConstants.*;
+import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
if (handler != null) {
handler.getBridgeFullState().thenAccept(fullState -> {
stopScan();
- removeOlderResults(getTimestampOfLastScan());
fullState.ifPresent(state -> {
state.sensors.forEach(this::addSensor);
state.lights.forEach(this::addLight);
}
}
+ @Override
+ protected synchronized void stopScan() {
+ removeOlderResults(getTimestampOfLastScan());
+ super.stopScan();
+ }
+
@Override
protected void startBackgroundDiscovery() {
final ScheduledFuture<?> scanningJob = this.scanningJob;
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);
}
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\\[\\]]", ""));
}
} 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
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;
@Override
public void deactivate() {
+ removeOlderResults(new Date().getTime());
super.deactivate();
}
}
@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 = "";
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
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}.
*/
@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 + '}';
}
}
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;
/**
--- /dev/null
+/**
+ * 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;
+ }
+}
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 + "}";
}
}
/** 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. */
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. */
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. */
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) + "}";
}
}
public @Nullable Integer heatsetpoint;
public @Nullable ThermostatMode mode;
public @Nullable Integer offset;
+ public @Nullable Boolean locked;
+ public @Nullable Boolean externalwindowopen;
}
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;
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;
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) {
}
/**
- * Stops the API request
+ * Stops the initialization request
*/
private void stopInitializationJob() {
ScheduledFuture<?> future = initializationJob;
}
}
- 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;
}
}
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);
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();
}
}
+ /**
+ * 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,
*
* @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)
*/
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();
}
});
}
+ 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();
}
}
}
- 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);
}
}
}
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;
import java.net.SocketTimeoutException;
import java.util.Collection;
-import java.util.Collections;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
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;
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;
*/
@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
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;
}
}
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) {
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 {
// 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) {
}
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;
});
* 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);
}
/**
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 {
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;
}
/**
*
* @param endPoint the endpoint (e.g. "lights/2/state")
* @param object the object (or null if no object)
+ * @param httpMethod the HTTP Method
* @return CompletableFuture of the result
*/
- public CompletableFuture<AsyncHttpClient.Result> sendObject(String endPoint, @Nullable Object object) {
+ public CompletableFuture<AsyncHttpClient.Result> sendObject(String endPoint, @Nullable Object object,
+ HttpMethod httpMethod) {
String json = object == null ? null : gson.toJson(object);
String url = buildUrl(config.host, config.httpPort, config.apikey, endPoint);
- logger.trace("Sending {} via {}", json, url);
+ logger.trace("Sending {} via {} to {}", json, httpMethod, url);
+
+ if (httpMethod == HttpMethod.PUT) {
+ return http.put(url, json, config.timeout);
+ } else if (httpMethod == HttpMethod.POST) {
+ return http.post(url, json, config.timeout);
+ } else if (httpMethod == HttpMethod.DELETE) {
+ return http.delete(url, config.timeout);
+ }
- return http.put(url, json, config.timeout);
+ return CompletableFuture.failedFuture(new IllegalArgumentException("Unknown HTTP Method"));
}
}
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;
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;
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);
@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;
}
}
}
+ } 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);
+ }
}
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;
*/
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
@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 {
}
if (command instanceof RefreshType) {
- valueUpdated(channelUID.getId(), lightStateCache);
+ valueUpdated(channelUID, lightStateCache);
return;
}
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;
} 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) {
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) {
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;
@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;
}
}
+ ThingBuilder thingBuilder = editThing();
+ boolean thingEdited = false;
+
LightState lightState = lightMessage.state;
- if (lightState != null && lightState.effect != null) {
- checkAndUpdateEffectChannels(lightMessage);
+ if (lightState != null && lightState.effect != null
+ && checkAndUpdateEffectChannels(thingBuilder, lightMessage)) {
+ thingEdited = true;
+ }
+
+ if (checkLastSeen(thingBuilder, stateResponse.lastseen)) {
+ thingEdited = true;
+ }
+ if (thingEdited) {
+ updateThing(thingBuilder.build());
}
- messageReceived(config.id, lightMessage);
+ messageReceived(lightMessage);
}
private enum EffectLightModel {
LIDL_MELINARA,
TINT_MUELLER,
- UNKNOWN;
+ UNKNOWN
}
- private void checkAndUpdateEffectChannels(LightMessage lightMessage) {
- EffectLightModel model = EffectLightModel.UNKNOWN;
+ private boolean checkAndUpdateEffectChannels(ThingBuilder thingBuilder, LightMessage lightMessage) {
// try to determine which model we have
- if (lightMessage.manufacturername.equals("_TZE200_s8gkrkxk")) {
- // the LIDL Melinara string does not report a proper model name
- model = EffectLightModel.LIDL_MELINARA;
- } else if (lightMessage.manufacturername.equals("MLI")) {
- model = EffectLightModel.TINT_MUELLER;
- } else {
+ EffectLightModel model = switch (lightMessage.manufacturername) {
+ case "_TZE200_s8gkrkxk" -> EffectLightModel.LIDL_MELINARA;
+ case "MLI" -> EffectLightModel.TINT_MUELLER;
+ default -> EffectLightModel.UNKNOWN;
+ };
+ if (model == EffectLightModel.UNKNOWN) {
logger.debug(
"Could not determine effect light type for thing {}, if you feel this is wrong request adding support on GitHub.",
thing.getUID());
}
ChannelUID effectChannelUID = new ChannelUID(thing.getUID(), CHANNEL_EFFECT);
- createChannel(CHANNEL_EFFECT, ChannelKind.STATE);
+
+ boolean thingEdited = false;
+
+ if (thing.getChannel(CHANNEL_EFFECT) == null) {
+ createChannel(thingBuilder, CHANNEL_EFFECT, ChannelKind.STATE);
+ thingEdited = true;
+ }
switch (model) {
case LIDL_MELINARA:
- // additional channels
- createChannel(CHANNEL_EFFECT_SPEED, ChannelKind.STATE);
+ if (thing.getChannel(CHANNEL_EFFECT_SPEED) == null) {
+ // additional channels
+ createChannel(thingBuilder, CHANNEL_EFFECT_SPEED, ChannelKind.STATE);
+ thingEdited = true;
+ }
List<String> options = List.of("none", "steady", "snow", "rainbow", "snake", "tinkle", "fireworks",
"flag", "waves", "updown", "vintage", "fading", "collide", "strobe", "sparkles", "carnival",
options = List.of("none", "colorloop");
commandDescriptionProvider.setCommandOptions(effectChannelUID, toCommandOptionList(options));
}
+
+ return thingEdited;
}
private List<CommandOption> toCommandOptionList(List<String> options) {
return options.stream().map(c -> new CommandOption(c, c)).collect(Collectors.toList());
}
- private void valueUpdated(String channelId, LightState newState) {
- Integer bri = newState.bri;
- Integer hue = newState.hue;
- Integer sat = newState.sat;
+ private void valueUpdated(ChannelUID channelUID, LightState newState) {
Boolean on = newState.on;
- switch (channelId) {
- case CHANNEL_ALERT:
- String alert = newState.alert;
- if (alert != null) {
- updateState(channelId, new StringType(alert));
- }
- break;
- case CHANNEL_SWITCH:
- case CHANNEL_LOCK:
- if (on != null) {
- updateState(channelId, OnOffType.from(on));
- }
- break;
- case CHANNEL_COLOR:
- if (on != null && !on) {
- updateState(channelId, OnOffType.OFF);
- } else if (bri != null && "xy".equals(newState.colormode)) {
- final double @Nullable [] xy = newState.xy;
- if (xy != null && xy.length == 2) {
- HSBType color = HSBType.fromXY((float) xy[0], (float) xy[1]);
- updateState(channelId, new HSBType(color.getHue(), color.getSaturation(), toPercentType(bri)));
- }
- } else if (bri != null && hue != null && sat != null) {
- updateState(channelId,
- new HSBType(new DecimalType(hue / HUE_FACTOR), toPercentType(sat), toPercentType(bri)));
- }
- break;
- case CHANNEL_BRIGHTNESS:
- if (bri != null && on != null && on) {
- updateState(channelId, toPercentType(bri));
- } else {
- updateState(channelId, OnOffType.OFF);
- }
- break;
- case CHANNEL_COLOR_TEMPERATURE:
+ switch (channelUID.getId()) {
+ case CHANNEL_ALERT -> updateStringChannel(channelUID, newState.alert);
+ case CHANNEL_SWITCH, CHANNEL_LOCK -> updateSwitchChannel(channelUID, on);
+ case CHANNEL_COLOR -> updateColorChannel(channelUID, newState);
+ case CHANNEL_BRIGHTNESS -> updatePercentTypeChannel(channelUID, newState.bri, newState.on);
+ case CHANNEL_COLOR_TEMPERATURE -> {
Integer ct = newState.ct;
if (ct != null && ct >= ctMin && ct <= ctMax) {
- updateState(channelId, new DecimalType(miredToKelvin(ct)));
- }
- break;
- case CHANNEL_POSITION:
- if (bri != null) {
- updateState(channelId, toPercentType(bri));
- }
- break;
- case CHANNEL_EFFECT:
- String effect = newState.effect;
- if (effect != null) {
- updateState(channelId, new StringType(effect));
- }
- break;
- case CHANNEL_EFFECT_SPEED:
- Integer effectSpeed = newState.effectSpeed;
- if (effectSpeed != null) {
- updateState(channelId, new DecimalType(effectSpeed));
+ updateState(channelUID, new DecimalType(miredToKelvin(ct)));
}
- break;
- default:
+ }
+ case CHANNEL_POSITION -> updatePercentTypeChannel(channelUID, newState.bri, true); // always post value
+ case CHANNEL_EFFECT -> updateStringChannel(channelUID, newState.effect);
+ case CHANNEL_EFFECT_SPEED -> updateDecimalTypeChannel(channelUID, newState.effectSpeed);
}
}
@Override
- public void messageReceived(String sensorID, DeconzBaseMessage message) {
- if (message instanceof LightMessage) {
- LightMessage lightMessage = (LightMessage) message;
- logger.trace("{} received {}", thing.getUID(), lightMessage);
+ public void messageReceived(DeconzBaseMessage message) {
+ logger.trace("{} received {}", thing.getUID(), message);
+ if (message instanceof LightMessage lightMessage) {
LightState lightState = lightMessage.state;
if (lightState != null) {
if (lastCommandExpireTimestamp > System.currentTimeMillis()
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)));
+ }
+ }
}
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;
* 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();
@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());
// Some sensors support optional channels
// (see https://github.com/dresden-elektronik/deconz-rest-plugin/wiki/Supported-Devices#sensors)
// any battery-powered sensor
+ ThingBuilder thingBuilder = editThing();
+ boolean thingEdited = false;
+
if (sensorConfig.battery != null) {
- createChannel(CHANNEL_BATTERY_LEVEL, ChannelKind.STATE);
- createChannel(CHANNEL_BATTERY_LOW, ChannelKind.STATE);
+ if (createChannel(thingBuilder, CHANNEL_BATTERY_LEVEL, ChannelKind.STATE)) {
+ thingEdited = true;
+ }
+ if (createChannel(thingBuilder, CHANNEL_BATTERY_LOW, ChannelKind.STATE)) {
+ thingEdited = true;
+ }
+ } else if (sensorState.lowbattery != null) {
+ // if sensorConfig.battery != null the channel is already added
+ if (createChannel(thingBuilder, CHANNEL_BATTERY_LOW, ChannelKind.STATE)) {
+ thingEdited = true;
+ }
}
- if (sensorState.lowbattery != null) {
- createChannel(CHANNEL_BATTERY_LOW, ChannelKind.STATE);
+ if (createTypeSpecificChannels(thingBuilder, sensorConfig, sensorState)) {
+ thingEdited = true;
}
- createTypeSpecificChannels(sensorConfig, sensorState);
-
- ignoreConfigurationUpdate = false;
+ if (checkLastSeen(thingBuilder, sensorMessage.lastseen)) {
+ thingEdited = true;
+ }
- // "Last seen" is the last "ping" from the device, whereas "last update" is the last status changed.
- // For example, for a fire sensor, the device pings regularly, without necessarily updating channels.
- // So to monitor a sensor is still alive, the "last seen" is necessary.
- // Because "last seen" is never updated by the WebSocket API - if this is supported, then we have to
- // manually poll it after the defined time
- String lastSeen = sensorMessage.lastseen;
- if (lastSeen != null && config.lastSeenPolling > 0) {
- createChannel(CHANNEL_LAST_SEEN, ChannelKind.STATE);
- updateState(CHANNEL_LAST_SEEN, Util.convertTimestampToDateTime(lastSeen));
- lastSeenPollingJob = scheduler.schedule(() -> requestState(this::processLastSeen), config.lastSeenPolling,
- TimeUnit.MINUTES);
- logger.trace("lastSeen polling enabled for thing {} with interval of {} minutes", thing.getUID(),
- config.lastSeenPolling);
+ // if the thing was edited, we update it now
+ if (thingEdited) {
+ logger.debug("Thing configuration changed, updating thing.");
+ updateThing(thingBuilder.build());
}
+ ignoreConfigurationUpdate = false;
// Initial data
updateChannels(sensorConfig);
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
*
*/
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
+ }
}
}
*/
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) {
}
private void updateChannels(SensorConfig newConfig) {
+ this.sensorConfig = newConfig;
List<String> configChannels = getConfigChannels();
thing.getChannels().stream().map(Channel::getUID)
.filter(channelUID -> configChannels.contains(channelUID.getId()))
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));
- }
}
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;
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);
}
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 {
} 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);
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);
+ }
+ }
}
}
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
@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);
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;
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;
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);
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;
+ }
}
}
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;
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
* @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);
}
}
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;
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;
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;
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) {
return;
} else if (connectionState == ConnectionState.DISCONNECTING) {
logger.warn("{} trying to re-connect while still disconnecting", socketName);
+ return;
}
try {
+ connectionState = ConnectionState.CONNECTING;
URI destUri = URI.create("ws://" + ip);
client.start();
logger.debug("Trying to connect {} to {}", socketName, destUri);
client.connect(this, destUri).get();
} catch (Exception e) {
- connectionListener.connectionLost("Error while connecting: " + e.getMessage());
+ String reason = "Error while connecting: " + e.getMessage();
+ if (e.getMessage() == null) {
+ logger.warn("{}: {}", socketName, reason, e);
+ } else {
+ logger.warn("{}: {}", socketName, reason);
+ }
+ connectionListener.webSocketConnectionLost(reason);
+ }
+ }
+
+ private void startOrResetWatchdogTimer() {
+ stopWatchdogTimer(); // stop already running timer
+ watchdogJob = scheduler.schedule(
+ () -> connectionListener.webSocketConnectionLost(
+ "Watchdog timed out after " + watchdogInterval + "s. Websocket seems to be dead."),
+ watchdogInterval, TimeUnit.SECONDS);
+ }
+
+ private void stopWatchdogTimer() {
+ ScheduledFuture<?> watchdogTimer = this.watchdogJob;
+ if (watchdogTimer != null) {
+ watchdogTimer.cancel(false);
+ this.watchdogJob = null;
}
}
- public void close() {
+ /**
+ * dispose the websocket (close connection and destroy client)
+ *
+ */
+ public void dispose() {
+ stopWatchdogTimer();
try {
connectionState = ConnectionState.DISCONNECTING;
client.stop();
logger.debug("{} encountered an error while closing connection", socketName, e);
}
client.destroy();
+ connectionState = ConnectionState.DISCONNECTED;
}
public void registerListener(ResourceType resourceType, String sensorID, WebSocketMessageListener listener) {
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 {
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.",
return;
}
+ // we still need the original resource type here
Class<? extends DeconzBaseMessage> expectedMessageType = changedMessage.r.getExpectedMessageType();
if (expectedMessageType == null) {
logger.warn(
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,
@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");
}
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
/**
* 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);
}
/**
* 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);
}
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;
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
*/
.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;
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,
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
*/
.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;
xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0 https://openhab.org/schemas/config-description-1.0.0.xsd">
<config-description uri="thing-type:deconz:bridge">
+ <parameter-group name="http">
+ <label>HTTP Connection</label>
+ <advanced>true</advanced>
+ </parameter-group>
+ <parameter-group name="websocket">
+ <label>Websocket Connection</label>
+ <advanced>true</advanced>
+ </parameter-group>
<parameter name="host" type="text" required="true">
<label>Host Address</label>
<context>network-address</context>
<description>IP address or host name of deCONZ interface.</description>
</parameter>
- <parameter name="httpPort" type="integer" min="1" max="65535">
- <label>HTTP Port</label>
- <description>Port of the deCONZ HTTP interface.</description>
- <default>80</default>
- </parameter>
- <parameter name="port" type="integer" min="1" max="65535">
- <label>Websocket Port</label>
- <description>Port of the deCONZ Websocket.</description>
- <advanced>true</advanced>
- </parameter>
<parameter name="apikey" type="text">
<label>API Key</label>
<context>password</context>
<description>If no API Key is provided, a new one will be requested. You need to authorize the access on the deCONZ
web interface.</description>
</parameter>
- <parameter name="timeout" type="integer" unit="ms" min="0">
+ <parameter name="httpPort" type="integer" min="1" max="65535" groupName="http">
+ <label>Port</label>
+ <description>Port of the deCONZ HTTP interface.</description>
+ <advanced>true</advanced>
+ <default>80</default>
+ </parameter>
+ <parameter name="timeout" type="integer" unit="ms" min="0" groupName="http">
<label>Timeout</label>
<description>Timeout for asynchronous HTTP requests (in milliseconds).</description>
<advanced>true</advanced>
<default>2000</default>
</parameter>
+ <parameter name="port" type="integer" min="1" max="65535" groupName="websocket">
+ <label>Port</label>
+ <description>Port of the deCONZ Websocket.</description>
+ <advanced>true</advanced>
+ </parameter>
+ <parameter name="websocketTimeout" type="integer" unit="s" min="30" groupName="websocket">
+ <label>Timeout</label>
+ <description>Timeout for the websocket connection (in seconds).</description>
+ <advanced>true</advanced>
+ <default>120</default>
+ </parameter>
+ </config-description>
+
+ <config-description uri="thing-type:deconz:lightgroup">
+ <parameter name="id" type="text" required="true">
+ <label>Device ID</label>
+ <description>The deCONZ bridge assigns an integer number ID to each group.</description>
+ </parameter>
+ <parameter name="transitiontime" type="decimal" min="0" unit="s">
+ <label>Transition Time</label>
+ <description>Time to move between two states. If empty, the default of the group is used. Resolution is 1/10 second.</description>
+ </parameter>
+ <parameter name="colormode" type="text">
+ <label>Color Mode</label>
+ <description>Override the default color mode (auto-detect)</description>
+ <options>
+ <option value="hs">HSB</option>
+ <option value="xy">XY</option>
+ </options>
+ <advanced>true</advanced>
+ </parameter>
</config-description>
<config-description uri="thing-type:deconz:sensor">
<label>Transition Time</label>
<description>Time to move between two states. If empty, the default of the device is used. Resolution is 1/10 second.</description>
</parameter>
+ <parameter name="lastSeenPolling" type="integer" min="0" unit="min">
+ <label>LastSeen Poll Interval</label>
+ <description>Interval to poll the deCONZ Gateway for this light's "last_seen" channel. Polling is disabled when set
+ to 0 (default: 1440, once per day).</description>
+ <default>1440</default>
+ </parameter>
</config-description>
<config-description uri="thing-type:deconz:colorlight">
</options>
<advanced>true</advanced>
</parameter>
- </config-description>
-
- <config-description uri="thing-type:deconz:lightgroup">
- <parameter name="id" type="text" required="true">
- <label>Device ID</label>
- <description>The deCONZ bridge assigns an integer number ID to each group.</description>
- </parameter>
- <parameter name="colormode" type="text">
- <label>Color Mode</label>
- <description>Override the default color mode (auto-detect)</description>
- <options>
- <option value="hs">HSB</option>
- <option value="xy">XY</option>
- </options>
- <advanced>true</advanced>
+ <parameter name="lastSeenPolling" type="integer" min="0" unit="min">
+ <label>LastSeen Poll Interval</label>
+ <description>Interval to poll the deCONZ Gateway for this light's "last_seen" channel. Polling is disabled when set
+ to 0 (default: 1440, once per day).</description>
+ <default>1440</default>
</parameter>
</config-description>
+
</config-description:config-descriptions>
# 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.
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
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
# 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
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
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
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
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
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
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.
<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"/>
<item-type>Switch</item-type>
<label>All On</label>
<description>"On" if all lights in this group are "On", otherwise "Off".</description>
+ <tags>
+ <tag>Lighting</tag>
+ </tags>
<state readOnly="true"/>
</channel-type>
<item-type>Switch</item-type>
<label>Any On</label>
<description>"On" if any light in this group is "On", otherwise "Off".</description>
+ <tags>
+ <tag>Lighting</tag>
+ </tags>
<state readOnly="true"/>
</channel-type>
<channel-type id="scene">
<item-type>String</item-type>
<label>Recall Scene</label>
+ <tags>
+ <tag>Lighting</tag>
+ </tags>
</channel-type>
</thing:thing-descriptions>
<bridge-type-ref id="deconz"/>
</supported-bridge-type-refs>
<label>Warning Device</label>
- <description>A warning device</description>
<category>Siren</category>
<channels>
- <channel id="alert" typeId="alert"></channel>
+ <channel typeId="alert" id="alert"/>
</channels>
<representation-property>uid</representation-property>
<supported-bridge-type-refs>
<bridge-type-ref id="deconz"/>
</supported-bridge-type-refs>
+
<label>On/Off Light</label>
<description>A light that can be turned on or off.</description>
+
<channels>
<channel typeId="system.power" id="switch"/>
<channel typeId="ontime" id="ontime"/>
</channels>
+ <properties>
+ <property name="thingTypeVersion">1</property>
+ </properties>
+
<representation-property>uid</representation-property>
<config-description-ref uri="thing-type:deconz:sensor"/>
<bridge-type-ref id="deconz"/>
</supported-bridge-type-refs>
<label>Dimmable Light</label>
- <description>A dimmable light.</description>
<category>Lightbulb</category>
<channels>
<channel typeId="system.brightness" id="brightness"/>
<channel typeId="ontime" id="ontime"/>
- <channel id="alert" typeId="alert"></channel>
+ <channel typeId="alert" id="alert"/>
</channels>
+ <properties>
+ <property name="thingTypeVersion">1</property>
+ </properties>
+
<representation-property>uid</representation-property>
<config-description-ref uri="thing-type:deconz:light"/>
<channel typeId="system.brightness" id="brightness"/>
<channel typeId="ct" id="color_temperature"/>
<channel typeId="ontime" id="ontime"/>
- <channel id="alert" typeId="alert"></channel>
+ <channel typeId="alert" id="alert"/>
</channels>
+ <properties>
+ <property name="thingTypeVersion">1</property>
+ </properties>
+
<representation-property>uid</representation-property>
<config-description-ref uri="thing-type:deconz:light"/>
<channels>
<channel typeId="system.color" id="color"/>
<channel typeId="ontime" id="ontime"/>
- <channel id="alert" typeId="alert"></channel>
+ <channel typeId="alert" id="alert"/>
</channels>
+ <properties>
+ <property name="thingTypeVersion">1</property>
+ </properties>
+
<representation-property>uid</representation-property>
<config-description-ref uri="thing-type:deconz:colorlight"/>
<channel typeId="system.color" id="color"/>
<channel typeId="ct" id="color_temperature"/>
<channel typeId="ontime" id="ontime"/>
- <channel id="alert" typeId="alert"></channel>
+ <channel typeId="alert" id="alert"/>
</channels>
+ <properties>
+ <property name="thingTypeVersion">1</property>
+ </properties>
+
<representation-property>uid</representation-property>
<config-description-ref uri="thing-type:deconz:colorlight"/>
<item-type>Number:Time</item-type>
<label>On Time</label>
<description>Time that the light stays on before switched off automatically (0=forever)</description>
+ <state pattern="%.1f %unit%" min="0"/>
</channel-type>
<channel-type id="effect">
<item-type>String</item-type>
<label>Effect Channel</label>
+ <tags>
+ <tag>Lighting</tag>
+ </tags>
</channel-type>
<channel-type id="effectSpeed">
<item-type>Number</item-type>
<label>Effect Speed Channel</label>
+ <tags>
+ <tag>Lighting</tag>
+ </tags>
<state min="0" max="10" step="1"/>
</channel-type>
<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"/>
<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"/>
<label>Power</label>
<description>Current power usage</description>
<category>Energy</category>
- <state readOnly="true" pattern="%.1f %unit%"></state>
+ <state readOnly="true" pattern="%.1f %unit%"/>
</channel-type>
<channel-type id="voltage">
<bridge-type-ref id="deconz"/>
</supported-bridge-type-refs>
<label>Consumption Sensor</label>
- <description>A consumption sensor</description>
<channels>
- <channel typeId="consumption" id="consumption"></channel>
- <channel typeId="last_updated" id="last_updated"></channel>
+ <channel typeId="consumption" id="consumption"/>
+ <channel typeId="last_updated" id="last_updated"/>
</channels>
<representation-property>uid</representation-property>
<label>Consumption</label>
<description>Current consumption</description>
<category>Energy</category>
- <state readOnly="true" pattern="%.1f %unit%"></state>
+ <state readOnly="true" pattern="%.1f %unit%"/>
</channel-type>
<thing-type id="colorcontrol">
<channel typeId="last_updated" id="last_updated"/>
</channels>
+ <properties>
+ <property name="thingTypeVersion">1</property>
+ </properties>
+
<representation-property>uid</representation-property>
<config-description-ref uri="thing-type:deconz:sensor"/>
<bridge-type-ref id="deconz"/>
</supported-bridge-type-refs>
<label>Switch/Button</label>
- <description>A switch or button</description>
<channels>
<channel typeId="buttonevent" id="buttonevent"/>
<channel typeId="button" id="button"/>
<item-type>Number</item-type>
<label>Button</label>
<description>The Button that was last pressed on the switch.</description>
- <state readOnly="true" pattern="%d"></state>
+ <state readOnly="true" pattern="%d"/>
</channel-type>
<channel-type id="gestureevent">
<bridge-type-ref id="deconz"/>
</supported-bridge-type-refs>
<label>Light Sensor</label>
- <description>A light sensor</description>
<channels>
<channel typeId="lightlux" id="lightlux"/>
<channel typeId="light_level" id="light_level"/>
<item-type>Number:Illuminance</item-type>
<label>Illuminance</label>
<description>Current light illuminance</description>
- <state readOnly="true" pattern="%.1f %unit%"></state>
+ <state readOnly="true" pattern="%.1f %unit%"/>
</channel-type>
<channel-type id="light_level" advanced="true">
<bridge-type-ref id="deconz"/>
</supported-bridge-type-refs>
<label>Temperature Sensor</label>
- <description>A temperature sensor</description>
<channels>
<channel typeId="temperature" id="temperature"/>
<channel typeId="last_updated" id="last_updated"/>
<label>Temperature</label>
<description>Current temperature</description>
<category>Temperature</category>
- <state readOnly="true" pattern="%.2f %unit%"></state>
+ <state readOnly="true" pattern="%.2f %unit%"/>
</channel-type>
<thing-type id="humiditysensor">
<bridge-type-ref id="deconz"/>
</supported-bridge-type-refs>
<label>Humidity Sensor</label>
- <description>A humidity sensor</description>
<channels>
<channel typeId="humidity" id="humidity"/>
<channel typeId="last_updated" id="last_updated"/>
<label>Humidity</label>
<description>Current humidity</description>
<category>Humidity</category>
- <state readOnly="true" pattern="%.2f %unit%"></state>
+ <state readOnly="true" pattern="%.2f %unit%"/>
</channel-type>
<thing-type id="pressuresensor">
<bridge-type-ref id="deconz"/>
</supported-bridge-type-refs>
<label>Pressure Sensor</label>
- <description>A pressure senor</description>
<channels>
- <channel typeId="pressure" id="pressure"></channel>
- <channel typeId="last_updated" id="last_updated"></channel>
+ <channel typeId="pressure" id="pressure"/>
+ <channel typeId="last_updated" id="last_updated"/>
</channels>
<representation-property>uid</representation-property>
<label>Pressure</label>
<description>Current pressure</description>
<category>Pressure</category>
- <state readOnly="true" pattern="%.1f %unit%"></state>
+ <state readOnly="true" pattern="%.1f %unit%"/>
</channel-type>
<thing-type id="daylightsensor">
<bridge-type-ref id="deconz"/>
</supported-bridge-type-refs>
<label>Daylight Sensor</label>
- <description>A daylight sensor</description>
<channels>
- <channel typeId="value" id="value"></channel>
- <channel typeId="light" id="light"></channel>
+ <channel typeId="value" id="value"/>
+ <channel typeId="light" id="light"/>
</channels>
<representation-property>uid</representation-property>
<item-type>Number</item-type>
<label>Daylight Value</label>
<description>Dawn is around 130, sunrise at 140, sunset at 190, and dusk at 210</description>
- <state readOnly="true"></state>
+ <state readOnly="true"/>
</channel-type>
<channel-type id="light">
<item-type>String</item-type>
<label>Lightlevel</label>
- <description>A light level</description>
<state readOnly="true">
<options>
<option value="daylight">Daylight</option>
<bridge-type-ref id="deconz"/>
</supported-bridge-type-refs>
<label>Open/Close Sensor</label>
- <description>An open/close sensor</description>
<channels>
<channel typeId="open" id="open"/>
<channel typeId="last_updated" id="last_updated"/>
<item-type>Contact</item-type>
<label>Open/Close</label>
<description>Open/Close detected</description>
- <state readOnly="true"></state>
+ <state readOnly="true"/>
</channel-type>
<thing-type id="waterleakagesensor">
<bridge-type-ref id="deconz"/>
</supported-bridge-type-refs>
<label>Water Leakage Sensor</label>
- <description>A water leakage sensor</description>
<channels>
<channel typeId="waterleakage" id="waterleakage"/>
<channel typeId="last_updated" id="last_updated"/>
<bridge-type-ref id="deconz"/>
</supported-bridge-type-refs>
<label>Fire Sensor</label>
- <description>A fire sensor</description>
<channels>
<channel typeId="fire" id="fire"/>
<channel typeId="last_updated" id="last_updated"/>
<bridge-type-ref id="deconz"/>
</supported-bridge-type-refs>
<label>Alarm Sensor</label>
- <description>An alarm sensor</description>
<channels>
<channel typeId="alarm" id="alarm"/>
<channel typeId="last_updated" id="last_updated"/>
<bridge-type-ref id="deconz"/>
</supported-bridge-type-refs>
<label>Vibration Sensor</label>
- <description>A vibration sensor</description>
<channels>
<channel typeId="vibration" id="vibration"/>
<channel typeId="last_updated" id="last_updated"/>
<bridge-type-ref id="deconz"/>
</supported-bridge-type-refs>
<label>Battery Sensor</label>
- <description>A battery sensor</description>
<channels>
<channel typeId="system.battery-level" id="battery_level"/>
<channel typeId="last_updated" id="last_updated"/>
</channels>
+ <properties>
+ <property name="thingTypeVersion">1</property>
+ </properties>
+
<representation-property>uid</representation-property>
<config-description-ref uri="thing-type:deconz:sensor"/>
<state readOnly="true"/>
</channel-type>
-
<thing-type id="airqualitysensor">
<supported-bridge-type-refs>
<bridge-type-ref id="deconz"/>
</supported-bridge-type-refs>
- <label>Air quality Sensor</label>
- <description>An air quality sensor</description>
+ <label>Carbon-monoxide Sensor</label>
<channels>
<channel typeId="airquality" id="airquality"/>
<channel typeId="airqualityppb" id="airqualityppb"/>
<channel-type id="airquality">
<item-type>String</item-type>
- <label>Air quality level</label>
+ <label>Air Quality</label>
<description>Current air quality level based on volatile organic compounds (VOCs) measurement. Example: good or poor,
...</description>
- <state readOnly="true" pattern="%s"></state>
+ <state readOnly="true"/>
</channel-type>
<channel-type id="airqualityppb">
<item-type>Number:Dimensionless</item-type>
- <label>Air quality in ppb</label>
+ <label>Air Quality (ppb)</label>
<description>Current air quality based on measurements of volatile organic compounds (VOCs). The measured value is
specified in ppb (parts per billion).</description>
- <state readOnly="true" pattern="%d"></state>
+ <state readOnly="true"/>
</channel-type>
+ <thing-type id="moisturesensor">
+ <supported-bridge-type-refs>
+ <bridge-type-ref id="deconz"/>
+ </supported-bridge-type-refs>
+ <label>Moisture Sensor</label>
+ <channels>
+ <channel typeId="moisture" id="moisture"/>
+ <channel typeId="last_updated" id="last_updated"/>
+ </channels>
+
+ <representation-property>uid</representation-property>
+
+ <config-description-ref uri="thing-type:deconz:sensor"/>
+ </thing-type>
+
+ <channel-type id="moisture">
+ <item-type>Number:Dimensionless</item-type>
+ <label>Moisture</label>
+ <description>Current moisture</description>
+ <state readOnly="true" pattern="%.1f %unit%"/>
+ </channel-type>
<thing-type id="thermostat">
<supported-bridge-type-refs>
<config-description-ref uri="thing-type:deconz:sensor"/>
</thing-type>
+ <channel-type id="locked">
+ <item-type>Switch</item-type>
+ <label>Locked</label>
+ <description>Status of this thermostat's child lock.</description>
+ <category>Lock</category>
+ </channel-type>
+ <channel-type id="windowopen">
+ <item-type>Contact</item-type>
+ <label>Window Open</label>
+ </channel-type>
+ <channel-type id="externalwindowopen">
+ <item-type>Contact</item-type>
+ <label>External Window Open</label>
+ </channel-type>
<channel-type id="heatsetpoint">
<item-type>Number:Temperature</item-type>
<label>Target Temperature</label>
<description>Target temperature</description>
<category>Heating</category>
- <state pattern="%.1f %unit%" step="0.5" max="28" min="6"></state>
+ <state pattern="%.1f %unit%" step="0.5" max="28" min="6"/>
</channel-type>
<channel-type id="mode">
<item-type>String</item-type>
<item-type>Number:Temperature</item-type>
<label>Offset</label>
<description>Temperature offset</description>
- <state pattern="%.2f %unit%" step="0.01"></state>
+ <state pattern="%.2f %unit%" step="0.01"/>
</channel-type>
<channel-type id="valve">
<item-type>Number:Dimensionless</item-type>
<label>Valve position</label>
<description>Current valve position</description>
- <state readOnly="true" pattern="%.0f %unit%"/>
+ <state readOnly="true" pattern="%.1f %unit%"/>
</channel-type>
</thing:thing-descriptions>
--- /dev/null
+<?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>
* @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;
--- /dev/null
+/**
+ * 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));
+ }
+}
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;
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
lightThingHandler.initialize();
- Mockito.verify(stateDescriptionProvider).setDescriptionFragment(eq(channelUID_ct), any());
+ Mockito.verify(stateDescriptionProvider).setDescriptionFragment(eq(channelUIDCt), any());
}
@Test
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
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
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
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")));
}
}
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;
SensorThingHandler sensorThingHandler = new SensorThingHandler(sensor, gson);
sensorThingHandler.setCallback(thingHandlerCallback);
- sensorThingHandler.messageReceived("", sensorMessage);
+ sensorThingHandler.messageReceived(sensorMessage);
Mockito.verify(thingHandlerCallback).stateUpdated(eq(channelUID), eq(OnOffType.ON));
}
sensorThingHandler.setCallback(thingHandlerCallback);
// ACT
- sensorThingHandler.messageReceived("", sensorMessage);
+ sensorThingHandler.messageReceived(sensorMessage);
// ASSERT
Mockito.verify(thingHandlerCallback).stateUpdated(eq(channelUID), eq(StringType.valueOf("good")));
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
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
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)));
"battery": 98,
"on": true,
"pending" : [],
- "reachable": false
+ "reachable": true
},
"ep": 1,
"etag": "717549a99371f3ea1a5f0b40f1537094",
--- /dev/null
+{
+ "action": {
+ "alert": "none",
+ "bri": 127,
+ "colormode": "hs",
+ "ct": 0,
+ "effect": "none",
+ "hue": 0,
+ "on": false,
+ "sat": 127,
+ "scene": null,
+ "xy": [
+ 0,
+ 0
+ ]
+ },
+ "devicemembership": [
+ "3"
+ ],
+ "etag": "586d2448a818aa7f6f3baa4907f43468",
+ "id": "1",
+ "lights": [],
+ "name": "RM01",
+ "scenes": [],
+ "state": {
+ "all_on": false,
+ "any_on": false
+ },
+ "type": "LightGroup"
+}
\ No newline at end of file
--- /dev/null
+{
+ "config": {
+ "battery": 85,
+ "displayflipped": null,
+ "heatsetpoint": 2500,
+ "locked": null,
+ "mode": "auto",
+ "offset": 0,
+ "on": true,
+ "reachable": true
+ },
+ "ep": 1,
+ "etag": "717549a99371f3ea1a5f0b40f1537094",
+ "lastseen": "2020-05-31T20:24:55.819",
+ "manufacturername": "Eurotronic",
+ "modelid": "SPZB0001",
+ "name": "Test Thermostat",
+ "state": {
+ "lastupdated": "2020-05-31T20:24:55.819",
+ "on": true,
+ "temperature": 1650,
+ "valve": 255
+ },
+ "swversion": "20191014",
+ "type": "ZHAThermostat",
+ "uniqueid": "00:15:8d:00:01:ff:8a:00-01-0201"
+}
\ No newline at end of file
"lastupdated": "2020-05-31T20:24:55.819",
"on": true,
"temperature": 1650,
- "valve": 255
+ "valve": 99
},
"swversion": "20191014",
"type": "ZHAThermostat",