[dirigera] Initial contribution (#17719)

* feature-squash

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>
pull/18733/merge
Bernd Weymann 2025-06-12 19:21:04 +02:00 committed by GitHub
parent dc3f53c0d5
commit 78c4962b6a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
171 changed files with 35841 additions and 0 deletions

View File

@ -84,6 +84,7 @@
/bundles/org.openhab.binding.deutschebahn/ @soenkekueper
/bundles/org.openhab.binding.digiplex/ @rmichalak
/bundles/org.openhab.binding.digitalstrom/ @openhab/add-ons-maintainers
/bundles/org.openhab.binding.dirigera/ @weymann
/bundles/org.openhab.binding.dlinksmarthome/ @MikeJMajor
/bundles/org.openhab.binding.dmx/ @openhab/add-ons-maintainers
/bundles/org.openhab.binding.dolbycp/ @Cybso

View File

@ -406,6 +406,11 @@
<artifactId>org.openhab.binding.digitalstrom</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.binding.dirigera</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.binding.dlinksmarthome</artifactId>

View File

@ -0,0 +1,13 @@
This content is produced and maintained by the openHAB project.
* Project home: https://www.openhab.org
== Declared Project Licenses
This program and the accompanying materials are made available under the terms
of the Eclipse Public License 2.0 which is available at
https://www.eclipse.org/legal/epl-2.0/.
== Source Code
https://github.com/openhab/openhab-addons

View File

@ -0,0 +1,734 @@
# DIRIGERA Binding
Binding supporting the DIRIGERA Gateway from IKEA.
## Supported Things
The DIRIGERA `bridge` is providing the connection to all devices and scenes.
Refer to below sections which devices are supported and are covered by `things` connected to the DIRIGERA bridge.
| ThingTypeUID | Description | Section | Products |
|-----------------------|------------------------------------------------------------|-------------------------------------------|-------------------------------------------|
| `gateway` | IKEA Gateway for smart products | [Gateway](#gateway-channels) | DIRIGERA |
| `air-purifier` | Air cleaning device with particle filter | [Air Purifier](#air-purifier) | STARKVIND |
| `air-quality` | Air measure for temperature, humidity and particles | [Sensors](#air-quality-sensor) | VINDSTYRKA |
| `blind` | Window or door blind | [Blinds](#blinds) | PRAKTLYSING ,KADRILJ ,FRYKTUR, TREDANSEN |
| `blind-controller` | Controller to open and close blinds | [Controller](#blind-controller) | TRÅDFRI |
| `switch-light` | Light with switch ON, OFF capability | [Lights](#switch-lights) | TRÅDFRI |
| `dimmable-light` | Light with brightness support | [Lights](#dimmable-lights) | TRÅDFRI |
| `temperature-light` | Light with color temperature support | [Lights](#temperature-lights) | TRÅDFRI, FLOALT |
| `color-light` | Light with color support | [Lights](#color-lights) | TRÅDFRI, ORMANÅS |
| `light-controller` | Controller to handle light attributes | [Controller](#light-controller) | TRÅDFRI, RODRET,STYRBAAR |
| `motion-sensor` | Sensor detecting motion events | [Sensors](#motion-sensor) | TRÅDFRI |
| `motion-light-sensor` | Sensor detecting motion events and measures light level | [Sensors](#motion-light-sensor) | VALLHORN |
| `single-shortcut` | Shortcut controller with one button | [Controller](#single-shortcut-controller) | TRÅDFRI |
| `double-shortcut` | Shortcut controller with two buttons | [Controller](#double-shortcut-controller) | SOMRIG |
| `simple-plug` | Power plug | [Plugs](#simple-plug) | TRÅDFRI, ÅSKVÄDER |
| `power-plug` | Power plug with status light and child lock | [Plugs](#power-plug) | TRETAKT |
| `smart-plug` | Power plug with electricity measurements | [Plugs](#smart-power-plug) | INSPELNING |
| `speaker` | Speaker with player activities | [Speaker](#speaker) | SYMFONISK |
| `sound-controller` | Controller for speakers | [Controller](#sound-controller) | SYMFONISK, TRÅDFRI |
| `contact-sensor` | Sensor tracking if windows or doors are open | [Sensors](#contact-sensor) | PARASOLL |
| `water-sensor` | Sensor to detect water leaks | [Sensors](#water-sensor) | BADRING |
| `repeater` | Repeater to strengthen signal | [Repeater](#repeater) | TRÅDFRI |
| `scene` | Scene from IKEA Home smart app which can be triggered | [Scenes](#scenes) | - |
## Discovery
The discovery will automatically detect your DIRIGERA Gateway via mDNS.
If it cannot be found check your router for IP address.
Manual scan isn't supported.
After successful creation of DIRIGERA Gateway and pairing process connected devices are automatically added to your INBOX.
You can switch off the automatic detection in [Bridge configuration](#bridge-configuration).
**Before adding the bridge** read [Pairing section](#gateway-pairing).
Devices connected to this bridge will be detected automatically unless you don't switch it off in [Bridge Configuration](#bridge-configuration)
## Gateway Bridge
### Bridge Configuration
| Name | Type | Description | Explanation | Default | Required |
|-----------------|---------|------------------------------------------------------------|--------------------------------------------------------------------------------------|---------|----------|
| `ipAddress` | text | DIRIGERA IP Address | Use discovery to obtain this value automatically or enter it manually if known | N/A | yes |
| `id` | text | Unique id of this gateway | Detected automatically after successful pairing | N/A | no |
| `discovery` | boolean | Configure if paired devices shall be detected by discovery | Run continuously in the background and detect new, deleted or changed devices | true | no |
### Gateway Pairing
First setup requires pairing the DIRIGERA gateway with openHAB.
You need physical access to the gateway to finish pairing so ensure you can reach it quickly.
Let's start pairing
1. Add the bridge found in discovery
2. Pairing started automatically after creation!
3. Press the button on the DIRIGERA rear side
4. Your bridge shall switch to ONLINE
### Gateway Channels
| Channel | Type | Read/Write | Description |
|-----------------|-----------|------------|----------------------------------------------|
| `pairing` | Switch | RW | Sets DIRIGERA hub into pairing mode |
| `location` | Location | R(W) | Location in lat.,lon. coordinates |
| `sunrise` | DateTime | R | Date and time of next sunrise |
| `sunset` | DateTime | R | Date and time of next sunset |
| `statistics` | String | R | Several statistics about gateway activities |
Channel `location` can overwrite GPS position with openHAB location, but it's not possible to delete GPS data.
See [Gateway Limitations](#gateway-limitations) for further information.
### Follow Sun
<img align="right" height="150" src="doc/follow-sun.png">
[Motion Sensors](#motion-sensor) can be active all the time or follow a schedule.
One schedule is follow the sun which needs to be activated in the IKEA Home smart app in _Hub Settings_.
## Things
With [DIRIGERA Gateway Bridge](#gateway-bridge) in place things can be connected as mentioned in the [supported things section](#supported-things).
Things contain generic [configuration](), [properties]() and [channels]() according to their capabilities.
### Generic Thing Configuration
Each thing is identified by a unique id which is mandatory to configure.
Discovery will automatically identify the id.
| Name | Type | Description | Default | Required |
|-------------------|---------|-------------------------------------|---------|----------|
| `id` | text | Unique id of this device / scene | N/A | yes |
### Generic Thing Properties
Each thing has properties attached for product information.
It contains information of hardware and firmware version, device model and manufacturer.
Device capabilities are listed in `canReceive` and `canSend`.
<img align="center" width="500" src="doc/thing-properties.png">
### Generic Thing Channels
#### OTA Channels
Over-the-Air (OTA) updates are common for many devices.
If device is providing these channels is detected during runtime.
| Channel | Type | Read/Write | Description | Advanced |
|-----------------|-----------|------------|----------------------------------------------|----------|
| `ota-status` | Number | R | Over-the-air overall status | |
| `ota-state` | Number | R | Over-the-air current state | X |
| `ota-progress` | Number | R | Over-the-air current progress | X |
`ota-status` shows the _overall status_ if your device is _up to date_ or an _update is available_.
`ota-state` and `ota-progress` shows more detailed information which you may want to follow, that's why they are declared as advanced channels.
**OTA Mappings**
Mappings for `ota-status`
- 0 : Up to date
- 1 : Update available
Mappings for `ota-state`
- 0 : Ready to check
- 1 : Check in progress
- 2 : Ready to download
- 3 : Download in progress
- 4 : Update in progress
- 5 : Update failed
- 6 : Ready to update
- 7 : Check failed
- 8 : Download failed
- 9 : Update complete
- 10 : Battery check failed
#### Links and Candidates
Devices can be connected directly e.g. sensors or controllers with lights, plugs, blinds or speakers.
It's detected during runtime if a device is capable to support links _and_ if devices are available in your system to support this connection.
The channels are declared advanced and can be used for setup procedure.
| Channel | Type | Read/Write | Description | Advanced |
|-----------------------|-----------------------|------------|--------------------------------------------------|----------|
| `links` | String | RW | Linked controllers and sensors | X |
| `link-candidates` | String | RW | Candidates which can be linked | X |
<img align="right" width="300" src="doc/link-candidates.png">
Several devices can be linked together like
- [Light Controller](#light-controller) and [Motion Sensors](#motion-sensor) to [Plugs](#power-plugs) and [Lights](#lights)
- [Blind Controller](#blind-controller) to [Blinds](#blinds)
- [Sound Controller](#sound-controller) to [Speakers](#speaker)
Established links are shown in channel `links`.
The linked devices can be clicked in the UI and the link will be removed.
Possible candidates to be linked are shown in channel `link-candidates`.
If a candidate is clicked in the UI the link will be established.
Candidates and links marked with `(!)` are not present in openHAB environment so no handler is created yet.
In this case it's possible not all links are shown in the UI, but the present ones shall work.
#### Other Channels
| Channel | Type | Read/Write | Description |
|-----------------------|-------------------|------------|----------------------------------------------|
| `startup` | Number | RW | Startup behavior after power cutoff |
| `custom-name` | String | RW | Name given from IKEA home smart app |
`startup` defines how the device shall behave after a power cutoff.
If there's a dedicated hardwired light switch which cuts power towards the bulb it makes sense to switch them on every time the switch is pressed.
But it's also possible to recover the last state.
Mappings for `startup`
- 0 : Previous
- 1 : On
- 2 : Off
- 3 : Switch
Option 3 is offered in IKEA Home smart app to control lights with using your normal light switch _slowly and smooth_.
With this the light shall stay online.
I wasn't able to reproduce this behavior.
Maybe somebody has more success.
`custom-name` is declared e.g. in your IKEA Home smart app.
This name is reflected in the discovery and if thing is created this name will be the thing label.
If `custom-name` is changed via openHAB API or a rule the label will not change.
### Unknown Devices
Filter your traces regarding 'DIRIGERA MODEL Unsupported Device'.
The trace contains a JSON object at the end which is needed to implement a corresponding handler.
## Air Purifier
Air cleaning device with particle filter.
| Channel | Type | Read/Write | Description |
|-----------------------|-------------------|------------|----------------------------------------------|
| `fan-mode` | Number | RW | Fan on, off, speed or automatic behavior |
| `fan-speed` | Dimmer | RW | Manual regulation of fan speed |
| `fan-runtime` | Number:Time | R | Fan runtime in minutes |
| `filter-elapsed` | Number:Time | R | Filter elapsed time in minutes |
| `filter-remain` | Number:Time | R | Time to filter replacement in minutes |
| `filter-lifetime` | Number:Time | R | Filter lifetime in minutes |
| `filter-alarm` | Switch | R | Filter alarm signal |
| `particulate-matter` | Number:Density | R | Category 2.5 particulate matter |
| `disable-status-light`| Switch | RW | Disable status light on plug |
| `child-lock` | Switch | RW | Child lock for button on plug |
There are several `Number:Time` which are delivered in minutes as default.
Note you can change the unit when connecting an item e.g. to `d` (days) for readability.
So you can check in a rule if your remaining filter time is going below 7 days instead of calculating minutes.
### Air Purifier Channel Mappings
Mappings for `fan-mode`
- 0 : Auto
- 1 : Low
- 2 : Medium
- 3 : High
- 4 : On
- 5 : Off
## Blinds
Window or door blind.
| Channel | Type | Read/Write | Description |
|-----------------------|-----------------------|------------|--------------------------------------------------|
| `blind-state` | Number | RW | State if blind is moving up, down or stopped |
| `blind-level` | Dimmer | RW | Current blind level |
| `battery-level` | Number:Dimensionless | R | Battery charge level in percent |
#### Blind Channel Mappings
Mappings for `blind-state`
- 0 : Stopped
- 1 : Up
- 2 : Down
## Lights
Light devices in several variants.
Can be light bulbs, LED stripes, remote driver and more.
Configuration contains
| Name | Type | Description | Default | Required |
|-------------------|---------|---------------------------------------------------------------------|---------|----------|
| `id` | text | Unique id of this device / scene | N/A | yes |
| `fadeTime` | integer | Required time for fade sequnce to color or brightness | 750 | yes |
| `fadeSequence` | integer | Define sequence if several light parameters are changed at once | 0 | yes |
`fadeTime` adjust fading time according to your device.
Current behavior shows commands are acknowledged while device is fading but not executed correctly.
So they need to be executed one after another.
Maybe an update of the DIRIGERA gateway will change the current behavior and you can reduce them afterwards.
`fadeSequence` is only for [Color Lights](#color-lights).
Through `hsb` channel it's possible to adapt color brightness at once.
Again due to fading times they need to be executed in a sequence.
You can choose between options
- 0: First brightness, then color
- 1: First color, then brightness
### Lights ON OFF Behavior
When light is ON each command will change the settings accordingly immediately.
During power OFF the lights will preserve some values until next power ON.
| Channel | Type | Behavior |
|-----------------------|---------------|---------------------------------------------------------------------------|
| `power` | ON | Switch ON, apply last / stored values |
| `brightness` | ON | Switch ON, apply last / stored values |
| `brightness` | value > 0 | Switch ON, apply this brightness, apply last / stored values |
| `color-temperature` | ON | Switch ON, apply last / stored values |
| `color-temperature` | any | Store value, brightness stays at previous level |
| `color` | ON | Switch ON, apply last / stored values |
| `color` | value > 0 | Switch ON, apply this brightness, apply last / stored values |
| `color` | h,s,b | Store color and brightness for next ON |
| outside | | Switch ON, apply last / stored values |
## Switch Lights
Light with switch ON, OFF capability
| Channel | Type | Read/Write | Description |
|-----------------------|-----------------------|------------|--------------------------------------------------|
| `power` | Switch | RW | Power state of light |
## Dimmable Lights
Light with brightness support.
| Channel | Type | Read/Write | Description |
|-----------------------|-----------------------|------------|--------------------------------------------------|
| `power` | Switch | RW | Power state of light |
| `brightness` | Dimmer | RW | Control brightness of light |
Channel `brightness` can receive
- ON / OFF
- numbers from 0 to 100 as percent where 0 will switch the light OFF, any other > 0 switches light ON
## Temperature Lights
Light with color temperature support.
| Channel | Type | Read/Write | Description | Advanced |
|---------------------------|-----------------------|------------|------------------------------------------------------|----------|
| `power` | Switch | RW | Power state of light | |
| `brightness` | Dimmer | RW | Control brightness of light | |
| `color-temperature` | Dimmer | RW | Color temperature from cold (0 %) to warm (100 %) | |
| `color-temperature-abs` | Number:Temperature | RW | Color temperature of a bulb in Kelvin | X |
## Color Lights
Light with color support.
| Channel | Type | Read/Write | Description | Advanced |
|---------------------------|-----------------------|------------|------------------------------------------------------|----------|
| `power` | Switch | RW | Power state of light | |
| `brightness` | Dimmer | RW | Brightness of light in percent | |
| `color-temperature` | Dimmer | RW | Color temperature from cold (0 %) to warm (100 %) | |
| `color-temperature-abs` | Number:Temperature | RW | Color temperature of a bulb in Kelvin | |
| `color` | Color | RW | Color of light with hue, saturation and brightness | X |
Channel `color` can receive
- ON / OFF
- numbers from 0 to 100 as brightness in percent where 0 will switch the light OFF, any other > 0 switches light ON
- triple values for hue, saturation, brightness
## Power Plugs
Power plugs in different variants.
## Simple Plug
Simple plug with control of power state and startup behavior.
| Channel | Type | Read/Write | Description |
|-----------------------|-----------------------|------------|----------------------------------------------|
| `power` | Switch | RW | Power state of plug |
## Power Plug
Power plug with control of power state, startup behavior, hardware on/off button and status light.
Same channels as [Simple Plug](#simple-plug) plus following channels.
| Channel | Type | Read/Write | Description |
|-----------------------|-----------------------|------------|----------------------------------------------|
| `child-lock` | Switch | RW | Child lock for button on plug |
| `disable-status-light`| Switch | RW | Disable status light on plug |
## Smart Power Plug
Smart plug like [Power Plug](#power-plug) plus measuring capability.
| Channel | Type | Read/Write | Description |
|-----------------------|---------------------------|------------|----------------------------------------------|
| `electric-power` | Number:Power | R | Electric power delivered by plug |
| `energy-total` | Number:Energy | R | Total energy consumption |
| `energy-reset` | Number:Energy | R | Energy consumption since last reset |
| `reset-date` | DateTime | RW | Date and time of last reset |
| `electric-current` | Number:ElectricCurrent | R | Electric current measured by plug |
| `electric-voltage` | Number:ElectricPotential | R | Electric potential of plug |
Smart plug provides `energy-total` measuring energy consumption over lifetime and `energy-reset` measuring energy consumption from `reset-date` till now.
Channel `reset-date` is writable and will set the date time to the timestamp of command execution.
Past and future timestamps are not possible and will be ignored.
## Sensors
Various sensors for detecting events and measuring.
## Motion Sensor
Sensor detecting motion events.
| Channel | Type | Read/Write | Description |
|-----------------------|-----------------------|------------|--------------------------------------------------|
| `motion` | Switch | R | Motion detected by the device |
| `active-duration` | Number:Time | RW | Keep connected devices active for this duration |
| `battery-level` | Number:Dimensionless | R | Battery charge level in percent |
| `schedule` | Number | RW | Schedule when the sensor shall be active |
| `schedule-start` | DateTime | RW | Start time of sensor activity |
| `schedule-end` | DateTime | RW | End time of sensor activity |
| `light-preset` | String | RW | Light presets for different times of the day |
When motion is detected via `motion` channel all connected devices from `links` channel will be active for the time configured in `active-duration`.
Standard duration is seconds if raw number is sent as command.
See [Motion Sensor Rules](#motion-sensor-rules) for further examples.
Mappings for `schedule`
- 0 : Always, sensor is always active
- 1 : Follow sun, sensor gets active at sunset and deactivates at sunrise
- 2 : Schedule, custom schedule with manual start and end time
If option 1, follow sun is selected ensure you gave the permission in the IKEA Home smart app to use your GPS position to calculate times for sunrise and sunset.
See [Light Controller](#light-controller) for light-preset`.
## Motion Light Sensor
Sensor detecting motion events and measures light level.
Same channels as [Motion Sensor](#motion-sensor) with an additional `illuminance` channel.
| Channel | Type | Read/Write | Description |
|-----------------------|-----------------------|------------|----------------------------------------------|
| `illuminance` | Number:Illuminance | R | Illuminance in Lux |
## Water Sensor
Sensor to detect water leaks.
| Channel | Type | Read/Write | Description |
|-----------------------|-----------------------|------------|----------------------------------------------|
| `leak` | Switch | R | Water leak detected |
| `battery-level` | Number:Dimensionless | R | Battery charge level in percent |
## Contact Sensor
Sensor tracking if windows or doors are open
| Channel | Type | Read/Write | Description |
|-----------------------|-----------------------|------------|----------------------------------------------|
| `contact` | Contact | R | State if door or window is open or closed |
| `battery-level` | Number:Dimensionless | R | Battery charge level in percent |
## Air Quality Sensor
Air measure for temperature, humidity and particles.
| Channel | Type | Read/Write | Description |
|-----------------------|-----------------------|------------|------------------------------------------------------|
| `temperature` | Number:Temperature | R | Air Temperature |
| `humidity` | Number:Dimensionless | R | Air Humidity |
| `particulate-matter` | Number:Density | R | Category 2.5 particulate matter |
| `voc-index` | Number | R | Relative VOC intensity compared to recent history |
The VOC Index mimics the human noses perception of odors with a relative intensity compared to recent history.
The VOC Index is also sensitive to odorless VOCs, but it cannot discriminate between them.
See more information in the [sensor description](https://sensirion.com/media/documents/02232963/6294E043/Info_Note_VOC_Index.pdf).
## Controller
Controller for lights, plugs, blinds, shortcuts and speakers.
## Single Shortcut Controller
Shortcut controller with one button.
| Channel | Type | Read/Write | Description |
|-----------------------|-----------------------|------------|----------------------------------------------|
| `button1` | trigger | | Trigger of first button |
| `battery-level` | Number:Dimensionless | R | Battery charge level in percent |
### Button Triggers
Triggers for `button1`
- SHORT_PRESSED
- DOUBLE_PRESSED
- LONG_PRESSED
## Double Shortcut Controller
Shortcut controller with two buttons.
| Channel | Type | Read/Write | Description |
|-----------------------|-----------------------|------------|----------------------------------------------|
| `button2` | trigger | | Trigger of second button |
Same as [Single Shortcut Controller](#single-shortcut-controller) with additional `button2` trigger channel.
## Light Controller
Controller to handle light attributes.
| Channel | Type | Read/Write | Description |
|-----------------------|-----------------------|------------|----------------------------------------------|
| `battery-level` | Number:Dimensionless | R | Battery charge level in percent |
| `light-preset` | String | RW | Light presets for different times of the day |
<img align="right" width="150" src="doc/light-presets.png">
Channel `light-preset` provides a JSON array with time an light settings for different times.
If light is switched on by the controller the light attributes for the configured time section is used.
This only works for connected devices shown in channel `links`.
IKEA provided some presets which can be selected but it's also possible to generate a custom schedule.
They are provided as options as strings
- Warm
- Slowdown
- Smooth
- Bright
This feature is from IKEA test center and not officially present in the IKEA Home smart app now.
## Blind Controller
Controller to open and close blinds.
| Channel | Type | Read/Write | Description |
|-----------------------|-----------------------|------------|----------------------------------------------|
| `battery-level` | Number:Dimensionless | R | Battery charge level in percent |
## Sound Controller
Controller for speakers.
| Channel | Type | Read/Write | Description |
|-----------------------|-----------------------|------------|----------------------------------------------|
| `battery-level` | Number:Dimensionless | R | Battery charge level in percent |
## Speaker
Speaker with player activities.
| Channel | Type | Read/Write | Description |
|-----------------------|-----------------------|------------|----------------------------------------------|
| `media-control` | Player | RW | Media control play, pause, next, previous |
| `volume` | Dimmer | RW | Handle volume in percent |
| `mute` | Switch | R(W) | Mute current audio without stop playing |
| `shuffle` | Switch | RW | Control shuffle mode |
| `crossfade` | Switch | RW | Cross fading between tracks |
| `repeat` | Number | RW | Over-the-air overall status |
| `media-title` | String | R | Title of a played media file |
| `image` | RawType | R | Current playing track image |
Channel `mute` should be writable but this isnn't the case now.
See [Known Limitations](#speaker-limitations).
## Repeater
Repeater to strengthen signal.
Sadly there's no further information like _signal strength_ available so only [OTA channels](#ota-channels) and [custom name](#other-channels) is available.
## Scenes
Scene from IKEA home smart app which can be triggered.
| Channel | Type | Read/Write | Description |
|-----------------------|-----------------------|------------|----------------------------------------------|
| `trigger` | Number | RW | Trigger / undo scene execution |
| `last-trigger` | DateTime | R | Date and time when last trigger occurred |
Scenes are defined in IKEA Home smart app and can be performed via `trigger` channel.
Two commands are defined:
- 0 : Trigger
- 1 : Undo
If command 0 (Trigger) is sent scene will be executed.
There's a 30 seconds time slot to send command 1 (Undo).
The countdown is updating `trigger` channel state which can be evaluated if an undo operation is still possible.
State will switch to `Undef` after countdown.
## Known Limitations
### Gateway Limitations
Gateway channel `location` is reflecting the state correctly but isn't writable.
The Model says it `canReceive` command `coordinates` but in fact sending responds `http status 400`.
Channel will stay in this binding hoping a DIRIGERA software update will resolve this issue.
### Speaker Limitations
Speaker channel `mute` is reflecting the state correctly but isn't writable.
The Model says it `canReceive` command `isMuted` but in fact sending responds `http status 400`.
If mute is performed on Sonos App the channel is updating correctly, but sending the command fails!
Channel will stay in this binding hoping a DIRIGERA software update will resolve this issue.
## Development and Testing
Debugging is essential for such a binding which supports many available products and needs to support future products.
General debug messages will overflow traces and it's hard to find relevant information.
To deal with these challenges commands for [openHAB console](https://www.openhab.org/docs/administration/console.html) are provided.
```
Usage: openhab:dirigera token - Get token from DIRIGERA hub
Usage: openhab:dirigera json [<deviceId> | all] - Print JSON data
Usage: openhab:dirigera debug [<deviceId> | all] [true | false] - Enable / disable detailed debugging for specific / all devices
```
### `token`
Prints the access token to communicate with DIRIGERA gateway as console output.
```
console> openhab:dirigera token
DIRIGERA Hub token: abcdef12345.......
```
With token available you can test your devices e.g. via curl commands.
```java
curl -X PATCH https://$YOUR_IP:8443/v1/devices/$DEVICE -H 'Authorization: Bearer $TOKEN' -H 'Content-Type: application/json' -d '[{"attributes":{"colorHue":280,"colorSaturation":1}}]' --insecure
```
Replace content in curl command with following variables:
- $YOUR_IP - IP address of DIRIGERA gateway
- $DEVICE - bulb id you want to control, take it from configuration
- $TOKEN - shortly stop / start DIRIGERA bridge and search for obtained token
### `json`
Get capabilities and current status for one `deviceId` or all devices.
Output is shown on console as JSON String.
```
console> openhab:dirigera json 3c8b0049-eb5c-4ea1-9da3-cdedc50366ef_1
{"deviceType":"light","isReachable":true,"capabilities":{"canReceive":["customName","isOn","lightLevel","colorTemperature", ...}
```
### `debug`
Enables or disables detailed logging for one `deviceId` or all devices.
Answer is `Done` if command is successfully executed.
If you operate with the device you can see requests and responses in openHAB Log Viewer.
If device cannot be found answer is `Device Id xyz not found `.
```
console> openhab:dirigera debug all true
Done
```
## Full Example
### Thing Configuration
```java
Bridge dirigera:gateway:myhome "My wonderful Home" [ ipAddress="1.2.3.4", discovery=true ] {
Thing temperature-light living-room-bulb "Living Room Table Lamp" [ id="aaaaaaaa-bbbb-xxxx-yyyy-zzzzzzzzzzzz"]
Thing smart-plug dishwasher "Dishwasher" [ id="zzzzzzzz-yyyy-xxxx-aaaa-bbbbbbbbbbbb"]
Thing motion-sensor bedroom-motion "Bedroom Motion" [ id="zzzzzzzz-yyyy-xxxx-aaaa-ffffffffffff"]
}
```
### Item Configuration
```java
Switch Bedroom_Motion_Detection { channel="dirigera:motion-sensor:myhome:bedroom-motion:motion" }
Number:Time Bedroom_Motion_Active_Duration { channel="dirigera:motion-sensor:myhome:bedroom-motion:active-duration" }
Number Bedroom_Motion_Schedule { channel="dirigera:motion-sensor:myhome:bedroom-motion:schedule" }
DateTime Bedroom_Motion_Schedule_Start { channel="dirigera:motion-sensor:myhome:bedroom-motion:schedule-start" }
DateTime Bedroom_Motion_Schedule_End { channel="dirigera:motion-sensor:myhome:bedroom-motion:schedule-end" }
Number:Dimensionless Bedroom_Motion_Battery_Level { channel="dirigera:motion-sensor:myhome:bedroom-motion:battery-level" }
Switch Table_Lamp_Power_State { channel="dirigera:temperature-light:myhome:living-room-bulb:power" }
Dimmer Table_Lamp_Brightness { channel="dirigera:temperature-light:myhome:living-room-bulb:brightness" }
Dimmer Table_Lamp_Temperature { channel="dirigera:temperature-light:myhome:living-room-bulb:color-temperature" }
Number Table_Lamp_Startup { channel="dirigera:temperature-light:myhome:living-room-bulb:startup" }
Number Table_Lamp_OTA_Status { channel="dirigera:temperature-light:myhome:living-room-bulb:ota-status" }
Number Table_Lamp_OTA_State { channel="dirigera:temperature-light:myhome:living-room-bulb:ota-state" }
Number Table_Lamp_OTA_Progress { channel="dirigera:temperature-light:myhome:living-room-bulb:ota-progress" }
Switch Dishwasher_Power_State { channel="dirigera:smart-plug:myhome:dishwasher:power" }
Switch Dishwasher_Child_lock { channel="dirigera:smart-plug:myhome:dishwasher:child-lock" }
Switch Dishwasher_Disable_Light { channel="dirigera:smart-plug:myhome:dishwasher:disable-light" }
Number:Power Dishwasher_Power { channel="dirigera:smart-plug:myhome:dishwasher:electric-power" }
Number:Energy Dishwasher_Energy_Total { channel="dirigera:smart-plug:myhome:dishwasher:energy-total" }
Number:Energy Dishwasher_Energy_Reset { channel="dirigera:smart-plug:myhome:dishwasher:energy-reset" }
Number:ElectricCurrent Dishwasher_Ampere { channel="dirigera:smart-plug:myhome:dishwasher:electric-current" }
Number:ElectricPotential Dishwasher_Voltage { channel="dirigera:smart-plug:myhome:dishwasher:electric-potential" }
Number Dishwasher_Startup { channel="dirigera:smart-plug:myhome:dishwasher:startup" }
Number Dishwasher_OTA_Status { channel="dirigera:smart-plug:myhome:dishwasher:ota-status" }
Number Dishwasher_OTA_State { channel="dirigera:smart-plug:myhome:dishwasher:ota-state" }
Number Dishwasher_OTA_Progress { channel="dirigera:smart-plug:myhome:dishwasher:ota-progress" }
```
### Rule Examples
#### Shortcut Controller Rules
Catch triggers from shortcut controller and trigger a scene.
```java
rule "Shortcut Button 1 Triggers"
when
Channel 'dirigera:double-shortcut:myhome:my-shortcut-controller:button1' triggered
then
logInfo("DIRIGERA","Button 1 {}",receivedEvent)
myhome-light-scene.sendCommand(0)
end
```
#### Motion Sensor Rules
Change the active duration time
```java
rule "Sensor configuration"
when
System started
then
logInfo("DIRIGERA","Configuring IKEA sensors")
// active duration = 180 seconds
Bedroom_Motion_Active_Duration.sendCommand(180)
// active duration = 3 minutes aka 180 seconds
Bedroom_Motion_Active_Duration.sendCommand("3 min")
end
```
## Credits
This work is based on [Leggin](https://github.com/Leggin/dirigera) and [dvdgeisler](https://github.com/dvdgeisler/DirigeraClient).
Without these contributions this binding wouldn't be possible!

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

View File

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.addons.reactor.bundles</artifactId>
<version>5.0.0-SNAPSHOT</version>
</parent>
<artifactId>org.openhab.binding.dirigera</artifactId>
<name>openHAB Add-ons :: Bundles :: Dirigera Binding</name>
<dependencies>
<!-- version needs to match with other projects like org.openhab.io.openhabcloud.pom.xml -->
<dependency>
<groupId>org.json</groupId>
<artifactId>json</artifactId>
<version>20231013</version>
<scope>compile</scope>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<features name="org.openhab.binding.dirigera-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
<repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
<feature name="openhab-binding-dirigera" description="Dirigera Binding" version="${project.version}">
<feature>openhab-runtime-base</feature>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.dirigera/${project.version}</bundle>
</feature>
</features>

View File

@ -0,0 +1,362 @@
/*
* Copyright (c) 2010-2025 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.dirigera.internal;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.ThingTypeUID;
/**
* The {@link Constants} class defines common constants, which are
* used across the whole binding.
*
* @author Bernd Weymann - Initial contribution
*/
@NonNullByDefault
public class Constants {
public static final String BINDING_ID = "dirigera";
// List of all Thing Type UIDs
public static final ThingTypeUID THING_TYPE_GATEWAY = new ThingTypeUID(BINDING_ID, "gateway");
public static final ThingTypeUID THING_TYPE_COLOR_LIGHT = new ThingTypeUID(BINDING_ID, "color-light");
public static final ThingTypeUID THING_TYPE_TEMPERATURE_LIGHT = new ThingTypeUID(BINDING_ID, "temperature-light");
public static final ThingTypeUID THING_TYPE_DIMMABLE_LIGHT = new ThingTypeUID(BINDING_ID, "dimmable-light");
public static final ThingTypeUID THING_TYPE_SWITCH_LIGHT = new ThingTypeUID(BINDING_ID, "switch-light");
public static final ThingTypeUID THING_TYPE_MOTION_SENSOR = new ThingTypeUID(BINDING_ID, "motion-sensor");
public static final ThingTypeUID THING_TYPE_LIGHT_SENSOR = new ThingTypeUID(BINDING_ID, "light-sensor");
public static final ThingTypeUID THING_TYPE_MOTION_LIGHT_SENSOR = new ThingTypeUID(BINDING_ID,
"motion-light-sensor");
public static final ThingTypeUID THING_TYPE_CONTACT_SENSOR = new ThingTypeUID(BINDING_ID, "contact-sensor");
public static final ThingTypeUID THING_TYPE_SIMPLE_PLUG = new ThingTypeUID(BINDING_ID, "simple-plug");
public static final ThingTypeUID THING_TYPE_POWER_PLUG = new ThingTypeUID(BINDING_ID, "power-plug");
public static final ThingTypeUID THING_TYPE_SMART_PLUG = new ThingTypeUID(BINDING_ID, "smart-plug");
public static final ThingTypeUID THING_TYPE_SPEAKER = new ThingTypeUID(BINDING_ID, "speaker");
public static final ThingTypeUID THING_TYPE_SCENE = new ThingTypeUID(BINDING_ID, "scene");
public static final ThingTypeUID THING_TYPE_REPEATER = new ThingTypeUID(BINDING_ID, "repeater");
public static final ThingTypeUID THING_TYPE_LIGHT_CONTROLLER = new ThingTypeUID(BINDING_ID, "light-controller");
public static final ThingTypeUID THING_TYPE_BLIND_CONTROLLER = new ThingTypeUID(BINDING_ID, "blind-controller");
public static final ThingTypeUID THING_TYPE_SOUND_CONTROLLER = new ThingTypeUID(BINDING_ID, "sound-controller");
public static final ThingTypeUID THING_TYPE_SINGLE_SHORTCUT_CONTROLLER = new ThingTypeUID(BINDING_ID,
"single-shortcut");
public static final ThingTypeUID THING_TYPE_DOUBLE_SHORTCUT_CONTROLLER = new ThingTypeUID(BINDING_ID,
"double-shortcut");
public static final ThingTypeUID THING_TYPE_AIR_PURIFIER = new ThingTypeUID(BINDING_ID, "air-purifier");
public static final ThingTypeUID THING_TYPE_AIR_QUALITY = new ThingTypeUID(BINDING_ID, "air-quality");
public static final ThingTypeUID THING_TYPE_WATER_SENSOR = new ThingTypeUID(BINDING_ID, "water-sensor");
public static final ThingTypeUID THING_TYPE_BLIND = new ThingTypeUID(BINDING_ID, "blind");
public static final ThingTypeUID THING_TYPE_UNKNNOWN = new ThingTypeUID(BINDING_ID, "unkown");
public static final ThingTypeUID THING_TYPE_NOT_FOUND = new ThingTypeUID(BINDING_ID, "not-found");
public static final ThingTypeUID THING_TYPE_IGNORE = new ThingTypeUID(BINDING_ID, "ignore");
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_GATEWAY,
THING_TYPE_COLOR_LIGHT, THING_TYPE_TEMPERATURE_LIGHT, THING_TYPE_DIMMABLE_LIGHT, THING_TYPE_MOTION_SENSOR,
THING_TYPE_CONTACT_SENSOR, THING_TYPE_SIMPLE_PLUG, THING_TYPE_POWER_PLUG, THING_TYPE_SMART_PLUG,
THING_TYPE_SPEAKER, THING_TYPE_SCENE, THING_TYPE_REPEATER, THING_TYPE_LIGHT_CONTROLLER,
THING_TYPE_BLIND_CONTROLLER, THING_TYPE_SOUND_CONTROLLER, THING_TYPE_SINGLE_SHORTCUT_CONTROLLER,
THING_TYPE_DOUBLE_SHORTCUT_CONTROLLER, THING_TYPE_MOTION_LIGHT_SENSOR, THING_TYPE_AIR_QUALITY,
THING_TYPE_AIR_PURIFIER, THING_TYPE_WATER_SENSOR, THING_TYPE_BLIND, THING_TYPE_SWITCH_LIGHT);
public static final Set<ThingTypeUID> IGNORE_THING_TYPES_UIDS = Set.of(THING_TYPE_LIGHT_SENSOR, THING_TYPE_IGNORE);
public static final List<String> THING_PROPERTIES = List.of("model", "manufacturer", "firmwareVersion",
"hardwareVersion", "serialNumber", "productCode");
public static final String WS_URL = "wss://%s:8443/v1";
public static final String BASE_URL = "https://%s:8443/v1";
public static final String OAUTH_URL = BASE_URL + "/oauth/authorize";
public static final String TOKEN_URL = BASE_URL + "/oauth/token";
public static final String HOME_URL = BASE_URL + "/home";
public static final String DEVICE_URL = BASE_URL + "/devices/%s";
public static final String SCENE_URL = BASE_URL + "/scenes/%s";
public static final String SCENES_URL = BASE_URL + "/scenes";
public static final String PROPERTY_IP_ADDRESS = "ipAddress";
public static final String PROPERTY_DEVICES = "devices";
public static final String PROPERTY_SCENES = "scenes";
public static final String PROPERTY_DEVICE_ID = "id";
public static final String PROPERTY_DEVICE_TYPE = "deviceType";
public static final String PROPERTY_TYPE = "type";
public static final String PROPERTY_TOKEN = "token";
public static final String PROPERTY_ATTRIBUTES = "attributes";
public static final String PROPERTY_OTA_STATUS = "otaStatus";
public static final String PROPERTY_OTA_STATE = "otaState";
public static final String PROPERTY_OTA_PROGRESS = "otaProgress";
public static final String PROPERTY_BATTERY_PERCENTAGE = "batteryPercentage";
public static final String PROPERTY_PERMIT_JOIN = "permittingJoin";
public static final String PROPERTY_STARTUP_BEHAVIOR = "startupOnOff";
public static final String PROPERTY_POWER_STATE = "isOn";
public static final String PROPERTY_CUSTOM_NAME = "customName";
public static final String PROPERTY_REMOTE_LINKS = "remoteLinks";
public static final String PROPERTY_EMPTY = "";
public static final String ATTRIBUTE_COLOR_MODE = "colorMode";
public static final String DEVICE_TYPE_GATEWAY = "gateway";
public static final String DEVICE_TYPE_SPEAKER = "speaker";
public static final String DEVICE_TYPE_REPEATER = "repeater";
public static final String DEVICE_TYPE_AIR_PURIFIER = "airPurifier";
public static final String DEVICE_TYPE_BLINDS = "blinds";
public static final String TYPE_USER_SCENE = "userScene";
public static final String TYPE_CUSTOM_SCENE = "customScene";
public static final String DEVICE_TYPE_LIGHT = "light";
public static final String DEVICE_TYPE_MOTION_SENSOR = "motionSensor";
public static final String DEVICE_TYPE_LIGHT_SENSOR = "lightSensor";
public static final String DEVICE_TYPE_CONTACT_SENSOR = "openCloseSensor";
public static final String DEVICE_TYPE_ENVIRONMENT_SENSOR = "environmentSensor";
public static final String DEVICE_TYPE_WATER_SENSOR = "waterSensor";
public static final String DEVICE_TYPE_OUTLET = "outlet";
public static final String DEVICE_TYPE_LIGHT_CONTROLLER = "lightController";
public static final String DEVICE_TYPE_BLIND_CONTROLLER = "blindsController";
public static final String DEVICE_TYPE_SOUND_CONTROLLER = "soundController";
public static final String DEVICE_TYPE_SHORTCUT_CONTROLLER = "shortcutController";
// Generic channels
public static final String CHANNEL_CUSTOM_NAME = "custom-name";
public static final String CHANNEL_LINKS = "links";
public static final String CHANNEL_LINK_CANDIDATES = "link-candidates";
public static final String CHANNEL_POWER_STATE = "power";
public static final String CHANNEL_STARTUP_BEHAVIOR = "startup";
public static final String CHANNEL_BATTERY_LEVEL = "battery-level";
public static final String CHANNEL_OTA_STATUS = "ota-status";
public static final String CHANNEL_OTA_STATE = "ota-state";
public static final String CHANNEL_OTA_PROGRESS = "ota-progress";
// Gateway channels
public static final String CHANNEL_LOCATION = "location";
public static final String CHANNEL_SUNRISE = "sunrise";
public static final String CHANNEL_SUNSET = "sunset";
public static final String CHANNEL_PAIRING = "pairing";
public static final String CHANNEL_STATISTICS = "statistics";
// Light channels
public static final String CHANNEL_LIGHT_BRIGHTNESS = "brightness";
public static final String CHANNEL_LIGHT_TEMPERATURE = "color-temperature";
public static final String CHANNEL_LIGHT_TEMPERATURE_ABS = "color-temperature-abs";
public static final String CHANNEL_LIGHT_COLOR = "color";
public static final String CHANNEL_LIGHT_PRESET = "light-preset";
// Sensor channels
public static final String CHANNEL_MOTION_DETECTION = "motion";
public static final String CHANNEL_LEAK_DETECTION = "leak";
public static final String CHANNEL_ILLUMINANCE = "illuminance";
public static final String CHANNEL_CONTACT = "contact";
public static final String CHANNEL_ACTIVE_DURATION = "active-duration";
public static final String CHANNEL_SCHEDULE = "schedule";
public static final String CHANNEL_SCHEDULE_START = "schedule-start";
public static final String CHANNEL_SCHEDULE_END = "schedule-end";
// Plug channels
public static final String CHANNEL_POWER = "electric-power";
public static final String CHANNEL_ENERGY_TOTAL = "energy-total";
public static final String CHANNEL_ENERGY_RESET = "energy-reset";
public static final String CHANNEL_ENERGY_RESET_DATE = "reset-date";
public static final String CHANNEL_CURRENT = "electric-current";
public static final String CHANNEL_POTENTIAL = "electric-voltage";
public static final String CHANNEL_CHILD_LOCK = "child-lock";
public static final String CHANNEL_DISABLE_STATUS_LIGHT = "disable-status-light";
// Speaker channels
public static final String CHANNEL_PLAYER = "media-control";
public static final String CHANNEL_VOLUME = "volume";
public static final String CHANNEL_MUTE = "mute";
public static final String CHANNEL_TRACK = "media-title";
public static final String CHANNEL_PLAY_MODES = "modes";
public static final String CHANNEL_SHUFFLE = "shuffle";
public static final String CHANNEL_REPEAT = "repeat";
public static final String CHANNEL_CROSSFADE = "crossfade";
public static final String CHANNEL_IMAGE = "image";
// Scene channels
public static final String CHANNEL_TRIGGER = "trigger";
public static final String CHANNEL_LAST_TRIGGER = "last-trigger";
// Air quality channels
public static final String CHANNEL_TEMPERATURE = "temperature";
public static final String CHANNEL_HUMIDITY = "humidity";
public static final String CHANNEL_PARTICULATE_MATTER = "particulate-matter";
public static final String CHANNEL_VOC_INDEX = "voc-index";
// Air purifier channels
public static final String CHANNEL_PURIFIER_FAN_MODE = "fan-mode";
public static final String CHANNEL_PURIFIER_FAN_SPEED = "fan-speed";
public static final String CHANNEL_PURIFIER_FAN_RUNTIME = "fan-runtime";
public static final String CHANNEL_PURIFIER_FAN_SEQUENCE = "fan-sequence";
public static final String CHANNEL_PURIFIER_FILTER_ELAPSED = "filter-elapsed";
public static final String CHANNEL_PURIFIER_FILTER_REMAIN = "filter-remain";
public static final String CHANNEL_PURIFIER_FILTER_LIFETIME = "filter-lifetime";
public static final String CHANNEL_PURIFIER_FILTER_ALARM = "filter-alarm";
// Blinds channels
public static final String CHANNEL_BLIND_LEVEL = "blind-level";
public static final String CHANNEL_BLIND_STATE = "blind-state";
// Shortcut channels
public static final String CHANNEL_BUTTON_1 = "button1";
public static final String CHANNEL_BUTTON_2 = "button2";
// Websocket update types
public static final String EVENT_TYPE_DEVICE_DISCOVERED = "deviceDiscovered";
public static final String EVENT_TYPE_DEVICE_ADDED = "deviceAdded";
public static final String EVENT_TYPE_DEVICE_CHANGE = "deviceStateChanged";
public static final String EVENT_TYPE_DEVICE_REMOVED = "deviceRemoved";
public static final String EVENT_TYPE_SCENE_CREATED = "sceneCreated";
public static final String EVENT_TYPE_SCENE_UPDATE = "sceneUpdated";
public static final String EVENT_TYPE_SCENE_DELETED = "sceneDeleted";
/**
* Maps connecting device attributes to channel ids
*/
// Mappings for ota
public static final Map<String, Integer> OTA_STATUS_MAP = Map.of("upToDate", 0, "updateAvailable", 1);
public static final Map<String, Integer> OTA_STATE_MAP = new HashMap<String, Integer>() {
private static final long serialVersionUID = 1L;
{
put("readyToCheck", 0);
put("checkInProgress", 1);
put("readyToDownload", 2);
put("downloadInProgress", 3);
put("updateInProgress", 4);
put("updateFailed", 5);
put("readyToUpdate", 6);
put("checkFailed", 7);
put("downloadFailed", 8);
put("updateComplete", 9);
put("batteryCheckFailed", 10);
}
};
// Mappings for startup behavior
public static final Map<String, Integer> STARTUP_BEHAVIOR_MAPPING = Map.of("startPrevious", 0, "startOn", 1,
"startOff", 2, "startToggle", 3);
public static final Map<Integer, String> STARTUP_BEHAVIOR_REVERSE_MAPPING = reverseStateMapping(
STARTUP_BEHAVIOR_MAPPING);
/**
* DIRIGERA property to openHAB channel mappings
*/
public static final Map<String, String> AIR_PURIFIER_MAP = new HashMap<String, String>() {
private static final long serialVersionUID = 1L;
{
put("fanMode", CHANNEL_PURIFIER_FAN_MODE);
put("motorState", CHANNEL_PURIFIER_FAN_SPEED);
put("motorRuntime", CHANNEL_PURIFIER_FAN_RUNTIME);
put("filterElapsedTime", CHANNEL_PURIFIER_FILTER_ELAPSED);
put("filterAlarmStatus", CHANNEL_PURIFIER_FILTER_ALARM);
put("filterLifetime", CHANNEL_PURIFIER_FILTER_LIFETIME);
put("statusLight", CHANNEL_DISABLE_STATUS_LIGHT);
put("childLock", CHANNEL_CHILD_LOCK);
put("currentPM25", CHANNEL_PARTICULATE_MATTER);
put(PROPERTY_CUSTOM_NAME, CHANNEL_CUSTOM_NAME);
}
};
public static final Map<String, String> AIR_QUALITY_MAP = Map.of(PROPERTY_CUSTOM_NAME, CHANNEL_CUSTOM_NAME,
"currentTemperature", CHANNEL_TEMPERATURE, "currentRH", CHANNEL_HUMIDITY, "currentPM25",
CHANNEL_PARTICULATE_MATTER, "vocIndex", CHANNEL_VOC_INDEX);
public static final Map<String, String> BLIND_CONTROLLER_MAP = Map.of(PROPERTY_CUSTOM_NAME, CHANNEL_CUSTOM_NAME,
"batteryPercentage", CHANNEL_BATTERY_LEVEL);
public static final Map<String, String> BLINDS_MAP = Map.of(PROPERTY_CUSTOM_NAME, CHANNEL_CUSTOM_NAME,
"blindsState", CHANNEL_BLIND_STATE, "batteryPercentage", CHANNEL_BATTERY_LEVEL, "blindsCurrentLevel",
CHANNEL_BLIND_LEVEL);
public static final Map<String, String> COLOR_LIGHT_MAP = Map.of(PROPERTY_CUSTOM_NAME, CHANNEL_CUSTOM_NAME,
PROPERTY_POWER_STATE, CHANNEL_POWER_STATE, "lightLevel", CHANNEL_LIGHT_BRIGHTNESS, "colorHue",
CHANNEL_LIGHT_COLOR, "colorSaturation", CHANNEL_LIGHT_COLOR, "colorTemperature", CHANNEL_LIGHT_TEMPERATURE,
PROPERTY_STARTUP_BEHAVIOR, CHANNEL_STARTUP_BEHAVIOR);;
public static final Map<String, String> CONTACT_SENSOR_MAP = Map.of(PROPERTY_CUSTOM_NAME, CHANNEL_CUSTOM_NAME,
"batteryPercentage", CHANNEL_BATTERY_LEVEL, "isOpen", CHANNEL_CONTACT);
public static final Map<String, String> LIGHT_CONTROLLER_MAP = Map.of(PROPERTY_CUSTOM_NAME, CHANNEL_CUSTOM_NAME,
"batteryPercentage", CHANNEL_BATTERY_LEVEL, "circadianPresets", CHANNEL_LIGHT_PRESET);
public static final Map<String, String> LIGHT_SENSOR_MAP = Map.of(PROPERTY_CUSTOM_NAME, CHANNEL_CUSTOM_NAME,
"illuminance", CHANNEL_ILLUMINANCE);
public static final Map<String, String> MOTION_LIGHT_SENSOR_MAP = Map.of(PROPERTY_CUSTOM_NAME, CHANNEL_CUSTOM_NAME,
"batteryPercentage", CHANNEL_BATTERY_LEVEL, "isDetected", CHANNEL_MOTION_DETECTION, "illuminance",
CHANNEL_ILLUMINANCE, "sensorConfig", CHANNEL_ACTIVE_DURATION, "schedule", CHANNEL_SCHEDULE,
"schedule-start", CHANNEL_SCHEDULE_START, "schedule-end", CHANNEL_SCHEDULE_END, "circadianPresets",
CHANNEL_LIGHT_PRESET);
public static final Map<String, String> MOTION_SENSOR_MAP = Map.of(PROPERTY_CUSTOM_NAME, CHANNEL_CUSTOM_NAME,
"batteryPercentage", CHANNEL_BATTERY_LEVEL, "isDetected", CHANNEL_MOTION_DETECTION, "sensorConfig",
CHANNEL_ACTIVE_DURATION, "schedule", CHANNEL_SCHEDULE, "schedule-start", CHANNEL_SCHEDULE_START,
"schedule-end", CHANNEL_SCHEDULE_END, "circadianPresets", CHANNEL_LIGHT_PRESET);
public static final Map<String, String> REPEATER_MAP = Map.of(PROPERTY_CUSTOM_NAME, CHANNEL_CUSTOM_NAME);
public static final Map<String, String> SCENE_MAP = Map.of("lastTriggered", CHANNEL_TRIGGER);
public static final Map<String, String> SHORTCUT_CONTROLLER_MAP = Map.of(PROPERTY_CUSTOM_NAME, CHANNEL_CUSTOM_NAME,
"batteryPercentage", CHANNEL_BATTERY_LEVEL);
public static final Map<String, String> SMART_PLUG_MAP = new HashMap<String, String>() {
private static final long serialVersionUID = 1L;
{
put("isOn", CHANNEL_POWER_STATE);
put("currentActivePower", CHANNEL_POWER);
put("currentVoltage", CHANNEL_POTENTIAL);
put("currentAmps", CHANNEL_CURRENT);
put("totalEnergyConsumed", CHANNEL_ENERGY_TOTAL);
put("energyConsumedAtLastReset", CHANNEL_ENERGY_RESET);
put("timeOfLastEnergyReset", CHANNEL_ENERGY_RESET_DATE);
put("statusLight", CHANNEL_DISABLE_STATUS_LIGHT);
put("childLock", CHANNEL_CHILD_LOCK);
put(PROPERTY_STARTUP_BEHAVIOR, CHANNEL_STARTUP_BEHAVIOR);
put(PROPERTY_CUSTOM_NAME, CHANNEL_CUSTOM_NAME);
}
};
public static final Map<String, String> SOUND_CONTROLLER_MAP = Map.of(PROPERTY_CUSTOM_NAME, CHANNEL_CUSTOM_NAME,
"batteryPercentage", CHANNEL_BATTERY_LEVEL);
public static final Map<String, String> SPEAKER_MAP = Map.of("playback", CHANNEL_PLAYER, "volume", CHANNEL_VOLUME,
"isMuted", CHANNEL_MUTE, "playbackAudio", CHANNEL_TRACK, "playbackModes", CHANNEL_PLAY_MODES,
PROPERTY_CUSTOM_NAME, CHANNEL_CUSTOM_NAME);
public static final Map<String, String> TEMPERATURE_LIGHT_MAP = Map.of(PROPERTY_CUSTOM_NAME, CHANNEL_CUSTOM_NAME,
PROPERTY_POWER_STATE, CHANNEL_POWER_STATE, "lightLevel", CHANNEL_LIGHT_BRIGHTNESS, "colorTemperature",
CHANNEL_LIGHT_TEMPERATURE, PROPERTY_STARTUP_BEHAVIOR, CHANNEL_STARTUP_BEHAVIOR);
public static final Map<String, String> WATER_SENSOR_MAP = Map.of(PROPERTY_CUSTOM_NAME, CHANNEL_CUSTOM_NAME,
"batteryPercentage", CHANNEL_BATTERY_LEVEL, "waterLeakDetected", CHANNEL_LEAK_DETECTION);
public static Map<Integer, String> reverseStateMapping(Map<String, Integer> mapping) {
Map<Integer, String> reverseMap = new HashMap<>();
for (Map.Entry<String, Integer> entry : mapping.entrySet()) {
reverseMap.put(entry.getValue(), entry.getKey());
}
return reverseMap;
}
public static Map<String, String> reverse(Map<String, String> mapping) {
Map<String, String> reverseMap = new HashMap<>();
for (Map.Entry<String, String> entry : mapping.entrySet()) {
reverseMap.put(entry.getValue(), entry.getKey());
}
return reverseMap;
}
}

View File

@ -0,0 +1,41 @@
/*
* Copyright (c) 2010-2025 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.dirigera.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.events.EventPublisher;
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;
/**
* Dynamic provider of command options while leaving other state description fields as original.
*
* @author Bernd Weymann - Initial contribution
*/
@NonNullByDefault
@Component(service = { DynamicCommandDescriptionProvider.class, DirigeraCommandProvider.class })
public class DirigeraCommandProvider extends BaseDynamicCommandDescriptionProvider {
@Activate
public DirigeraCommandProvider(final @Reference EventPublisher eventPublisher, //
final @Reference ItemChannelLinkRegistry itemChannelLinkRegistry, //
final @Reference ChannelTypeI18nLocalizationService channelTypeI18nLocalizationService) {
this.eventPublisher = eventPublisher;
this.itemChannelLinkRegistry = itemChannelLinkRegistry;
this.channelTypeI18nLocalizationService = channelTypeI18nLocalizationService;
}
}

View File

@ -0,0 +1,174 @@
/*
* Copyright (c) 2010-2025 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.dirigera.internal;
import static org.openhab.binding.dirigera.internal.Constants.*;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.WWWAuthenticationProtocolHandler;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.openhab.binding.dirigera.internal.discovery.DirigeraDiscoveryService;
import org.openhab.binding.dirigera.internal.handler.DirigeraHandler;
import org.openhab.binding.dirigera.internal.handler.airpurifier.AirPurifierHandler;
import org.openhab.binding.dirigera.internal.handler.blind.BlindHandler;
import org.openhab.binding.dirigera.internal.handler.controller.BlindsControllerHandler;
import org.openhab.binding.dirigera.internal.handler.controller.DoubleShortcutControllerHandler;
import org.openhab.binding.dirigera.internal.handler.controller.LightControllerHandler;
import org.openhab.binding.dirigera.internal.handler.controller.ShortcutControllerHandler;
import org.openhab.binding.dirigera.internal.handler.controller.SoundControllerHandler;
import org.openhab.binding.dirigera.internal.handler.light.ColorLightHandler;
import org.openhab.binding.dirigera.internal.handler.light.DimmableLightHandler;
import org.openhab.binding.dirigera.internal.handler.light.SwitchLightHandler;
import org.openhab.binding.dirigera.internal.handler.light.TemperatureLightHandler;
import org.openhab.binding.dirigera.internal.handler.plug.PowerPlugHandler;
import org.openhab.binding.dirigera.internal.handler.plug.SimplePlugHandler;
import org.openhab.binding.dirigera.internal.handler.plug.SmartPlugHandler;
import org.openhab.binding.dirigera.internal.handler.repeater.RepeaterHandler;
import org.openhab.binding.dirigera.internal.handler.scene.SceneHandler;
import org.openhab.binding.dirigera.internal.handler.sensor.AirQualityHandler;
import org.openhab.binding.dirigera.internal.handler.sensor.ContactSensorHandler;
import org.openhab.binding.dirigera.internal.handler.sensor.MotionLightSensorHandler;
import org.openhab.binding.dirigera.internal.handler.sensor.MotionSensorHandler;
import org.openhab.binding.dirigera.internal.handler.sensor.WaterSensorHandler;
import org.openhab.binding.dirigera.internal.handler.speaker.SpeakerHandler;
import org.openhab.core.i18n.LocationProvider;
import org.openhab.core.storage.Storage;
import org.openhab.core.storage.StorageService;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerFactory;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Deactivate;
import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link DirigeraHandlerFactory} is responsible for creating things and thing
* handlers.
*
* @author Bernd Weymann - Initial contribution
*/
@NonNullByDefault
@Component(configurationPid = "binding.dirigera", service = ThingHandlerFactory.class)
public class DirigeraHandlerFactory extends BaseThingHandlerFactory {
private final Logger logger = LoggerFactory.getLogger(DirigeraHandlerFactory.class);
private final DirigeraStateDescriptionProvider stateProvider;
private final DirigeraDiscoveryService discoveryService;
private final DirigeraCommandProvider commandProvider;
private final LocationProvider locationProvider;
private final Storage<String> bindingStorage;
private final HttpClient insecureClient;
@Activate
public DirigeraHandlerFactory(@Reference StorageService storageService,
final @Reference DirigeraDiscoveryService discovery, final @Reference LocationProvider locationProvider,
final @Reference DirigeraCommandProvider commandProvider,
final @Reference DirigeraStateDescriptionProvider stateProvider) {
this.locationProvider = locationProvider;
this.commandProvider = commandProvider;
this.discoveryService = discovery;
this.stateProvider = stateProvider;
this.insecureClient = new HttpClient(new SslContextFactory.Client(true));
insecureClient.setUserAgentField(null);
try {
this.insecureClient.start();
// from https://github.com/jetty-project/jetty-reactive-httpclient/issues/33#issuecomment-777771465
insecureClient.getProtocolHandlers().remove(WWWAuthenticationProtocolHandler.NAME);
} catch (Exception e) {
// catching exception is necessary due to the signature of HttpClient.start()
logger.warn("DIRIGERA FACTORY Failed to start http client: {}", e.getMessage());
throw new IllegalStateException("Could not create HttpClient", e);
}
bindingStorage = storageService.getStorage(BINDING_ID);
}
@Deactivate
public void deactivate() {
try {
insecureClient.stop();
} catch (Exception e) {
logger.warn("Failed to stop http client: {}", e.getMessage());
}
}
@Override
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
}
@Override
protected @Nullable ThingHandler createHandler(Thing thing) {
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
if (THING_TYPE_GATEWAY.equals(thingTypeUID)) {
return new DirigeraHandler((Bridge) thing, insecureClient, bindingStorage, discoveryService,
locationProvider, commandProvider, bundleContext);
} else if (THING_TYPE_COLOR_LIGHT.equals(thingTypeUID)) {
return new ColorLightHandler(thing, COLOR_LIGHT_MAP, stateProvider);
} else if (THING_TYPE_TEMPERATURE_LIGHT.equals(thingTypeUID)) {
return new TemperatureLightHandler(thing, TEMPERATURE_LIGHT_MAP, stateProvider);
} else if (THING_TYPE_DIMMABLE_LIGHT.equals(thingTypeUID)) {
return new DimmableLightHandler(thing, TEMPERATURE_LIGHT_MAP);
} else if (THING_TYPE_SWITCH_LIGHT.equals(thingTypeUID)) {
return new SwitchLightHandler(thing, TEMPERATURE_LIGHT_MAP);
} else if (THING_TYPE_MOTION_SENSOR.equals(thingTypeUID)) {
return new MotionSensorHandler(thing, MOTION_SENSOR_MAP);
// } else if (THING_TYPE_LIGHT_SENSOR.equals(thingTypeUID)) {
// return new LightSensorHandler(thing, LIGHT_SENSOR_MAP);
} else if (THING_TYPE_MOTION_LIGHT_SENSOR.equals(thingTypeUID)) {
return new MotionLightSensorHandler(thing, MOTION_LIGHT_SENSOR_MAP);
} else if (THING_TYPE_CONTACT_SENSOR.equals(thingTypeUID)) {
return new ContactSensorHandler(thing, CONTACT_SENSOR_MAP);
} else if (THING_TYPE_SIMPLE_PLUG.equals(thingTypeUID)) {
return new SimplePlugHandler(thing, SMART_PLUG_MAP);
} else if (THING_TYPE_POWER_PLUG.equals(thingTypeUID)) {
return new PowerPlugHandler(thing, SMART_PLUG_MAP);
} else if (THING_TYPE_SMART_PLUG.equals(thingTypeUID)) {
return new SmartPlugHandler(thing, SMART_PLUG_MAP);
} else if (THING_TYPE_SPEAKER.equals(thingTypeUID)) {
return new SpeakerHandler(thing, SPEAKER_MAP);
} else if (THING_TYPE_SCENE.equals(thingTypeUID)) {
return new SceneHandler(thing, SCENE_MAP);
} else if (THING_TYPE_REPEATER.equals(thingTypeUID)) {
return new RepeaterHandler(thing, REPEATER_MAP);
} else if (THING_TYPE_LIGHT_CONTROLLER.equals(thingTypeUID)) {
return new LightControllerHandler(thing, LIGHT_CONTROLLER_MAP);
} else if (THING_TYPE_BLIND_CONTROLLER.equals(thingTypeUID)) {
return new BlindsControllerHandler(thing, BLIND_CONTROLLER_MAP);
} else if (THING_TYPE_SOUND_CONTROLLER.equals(thingTypeUID)) {
return new SoundControllerHandler(thing, SOUND_CONTROLLER_MAP);
} else if (THING_TYPE_SINGLE_SHORTCUT_CONTROLLER.equals(thingTypeUID)) {
return new ShortcutControllerHandler(thing, SHORTCUT_CONTROLLER_MAP, bindingStorage);
} else if (THING_TYPE_DOUBLE_SHORTCUT_CONTROLLER.equals(thingTypeUID)) {
return new DoubleShortcutControllerHandler(thing, SHORTCUT_CONTROLLER_MAP, bindingStorage);
} else if (THING_TYPE_AIR_QUALITY.equals(thingTypeUID)) {
return new AirQualityHandler(thing, AIR_QUALITY_MAP);
} else if (THING_TYPE_WATER_SENSOR.equals(thingTypeUID)) {
return new WaterSensorHandler(thing, WATER_SENSOR_MAP);
} else if (THING_TYPE_BLIND.equals(thingTypeUID)) {
return new BlindHandler(thing, BLINDS_MAP);
} else if (THING_TYPE_AIR_PURIFIER.equals(thingTypeUID)) {
return new AirPurifierHandler(thing, AIR_PURIFIER_MAP);
} else {
logger.debug("DIRIGERA FACTORY Request for {} doesn't match {}", thingTypeUID, THING_TYPE_GATEWAY);
return null;
}
}
}

View File

@ -0,0 +1,87 @@
/*
* Copyright (c) 2010-2025 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.dirigera.internal;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
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.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;
/**
* The {@link Clip2StateDescriptionProvider} provides dynamic state descriptions of alert, effect, scene, and colour
* temperature channels whose capabilities are dynamically determined at runtime.
*
* @author Andrew Fiddian-Green - Initial contribution
*
*/
@NonNullByDefault
@Component(service = { DynamicStateDescriptionProvider.class, DirigeraStateDescriptionProvider.class })
public class DirigeraStateDescriptionProvider extends BaseDynamicStateDescriptionProvider {
private Map<ChannelUID, StateDescriptionFragment> stateDescriptionMap = new HashMap<>();
@Activate
public DirigeraStateDescriptionProvider(final @Reference EventPublisher eventPublisher,
final @Reference ItemChannelLinkRegistry itemChannelLinkRegistry,
final @Reference ChannelTypeI18nLocalizationService channelTypeI18nLocalizationService) {
this.eventPublisher = eventPublisher;
this.itemChannelLinkRegistry = itemChannelLinkRegistry;
this.channelTypeI18nLocalizationService = channelTypeI18nLocalizationService;
}
@Override
public @Nullable StateDescription getStateDescription(Channel channel,
@Nullable StateDescription originalStateDescription, @Nullable Locale locale) {
StateDescription original = null;
StateDescriptionFragment fragment = stateDescriptionMap.get(channel.getUID());
if (fragment != null) {
original = fragment.toStateDescription();
StateDescription modified = super.getStateDescription(channel, original, locale);
if (modified == null) {
modified = original;
}
return modified;
}
return super.getStateDescription(channel, original, locale);
}
public void setStateDescription(ChannelUID channelUid, StateDescriptionFragment stateDescriptionFragment) {
StateDescription stateDescription = stateDescriptionFragment.toStateDescription();
if (stateDescription != null) {
StateDescriptionFragment old = stateDescriptionMap.get(channelUid);
stateDescriptionMap.put(channelUid, stateDescriptionFragment);
Set<String> linkedItems = null;
ItemChannelLinkRegistry compareRegistry = itemChannelLinkRegistry;
if (compareRegistry != null) {
linkedItems = compareRegistry.getLinkedItemNames(channelUid);
}
postEvent(ThingEventFactory.createChannelDescriptionChangedEvent(channelUid,
linkedItems != null ? linkedItems : Set.of(), stateDescriptionFragment, old));
}
}
}

View File

@ -0,0 +1,26 @@
/*
* Copyright (c) 2010-2025 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.dirigera.internal.config;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link BaseDeviceConfiguration} configuration for all devices
*
* @author Bernd Weymann - Initial contribution
*/
@NonNullByDefault
public class BaseDeviceConfiguration {
public String id = "";
}

View File

@ -0,0 +1,27 @@
/*
* Copyright (c) 2010-2025 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.dirigera.internal.config;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* {@link ColorLightConfiguration} configuration for lights with temperature or color attributes
*
* @author Bernd Weymann - Initial contribution
*/
@NonNullByDefault
public class ColorLightConfiguration extends BaseDeviceConfiguration {
public int fadeTime = 750;
public int fadeSequence = 0;
}

View File

@ -0,0 +1,32 @@
/*
* Copyright (c) 2010-2025 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.dirigera.internal.config;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link DirigeraConfiguration} class contains fields mapping thing configuration parameters.
*
* @author Bernd Weymann - Initial contribution
*/
@NonNullByDefault
public class DirigeraConfiguration extends BaseDeviceConfiguration {
public String ipAddress = "";
public boolean discovery = true;
@Override
public String toString() {
return "IP: " + ipAddress + ", ID: " + id;
}
}

View File

@ -0,0 +1,218 @@
/*
* Copyright (c) 2010-2025 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.dirigera.internal.console;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.dirigera.internal.Constants;
import org.openhab.binding.dirigera.internal.handler.DirigeraHandler;
import org.openhab.binding.dirigera.internal.interfaces.DebugHandler;
import org.openhab.core.io.console.Console;
import org.openhab.core.io.console.ConsoleCommandCompleter;
import org.openhab.core.io.console.StringsCompleter;
import org.openhab.core.io.console.extensions.AbstractConsoleCommandExtension;
import org.openhab.core.io.console.extensions.ConsoleCommandExtension;
import org.openhab.core.thing.ThingRegistry;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
/**
* The {@link DirigeraCommandExtension} is responsible for handling console commands.
*
* @author Bernd Weymann - Initial contribution
*/
@NonNullByDefault
@Component(service = ConsoleCommandExtension.class)
public class DirigeraCommandExtension extends AbstractConsoleCommandExtension {
private static final String CMD_TOKEN = "token";
private static final String CMD_JSON = "json";
private static final String CMD_DEBUG = "debug";
private static final List<String> COMMANDS = List.of(CMD_TOKEN, CMD_JSON, CMD_DEBUG);
private final ThingRegistry thingRegistry;
/**
* Provides a completer for the DIRIGERA console commands.
*
* @param thingRegistry the ThingRegistry to access things and their handlers
*/
private class DirigeraConsoleCommandCompleter implements ConsoleCommandCompleter {
@Override
public boolean complete(String[] args, int cursorArgumentIndex, int cursorPosition, List<String> candidates) {
if (cursorArgumentIndex <= 0) {
return new StringsCompleter(List.of(CMD_TOKEN, CMD_JSON, CMD_DEBUG), false).complete(args,
cursorArgumentIndex, cursorPosition, candidates);
} else if (cursorArgumentIndex == 1) {
List<String> options = new ArrayList<>();
options.add("all");
options.addAll(getDeviceIds());
return new StringsCompleter(options, false).complete(args, cursorArgumentIndex, cursorPosition,
candidates);
} else if (cursorArgumentIndex == 2) {
return new StringsCompleter(List.of("true", "false"), false).complete(args, cursorArgumentIndex,
cursorPosition, candidates);
}
return false;
}
}
/**
* Decodes the console command arguments and checks for validity.
*/
private class DirigeraConsoleCommandDecoder {
boolean valid = false;
String command = "";
String target = "";
boolean enable = false;
DirigeraConsoleCommandDecoder(String[] args) {
// Check parameter count and valid command, return immediately if invalid
if (args.length == 0 || args.length > 3) {
return;
}
command = args[0].toLowerCase();
if (!COMMANDS.contains(command)) {
return;
}
// Command is valid, check parameters
switch (command) {
case CMD_TOKEN:
// No parameters expected for token command
if (args.length == 1) {
valid = true;
}
break;
case CMD_JSON:
// Take second parameter for device ID or 'all'
if (args.length == 2) {
target = args[1].toLowerCase();
valid = true;
}
break;
case CMD_DEBUG:
// Three parameters expected for debug command, second as target and third as boolean
if (args.length == 3) {
target = args[1].toLowerCase();
String booleanCandidate = args[2].toLowerCase();
if (Boolean.TRUE.toString().toLowerCase().equals(booleanCandidate)
|| Boolean.FALSE.toString().toLowerCase().equals(booleanCandidate)) {
enable = Boolean.valueOf(booleanCandidate);
valid = true;
}
}
break;
}
}
}
@Activate
public DirigeraCommandExtension(final @Reference ThingRegistry thingRegistry) {
super(Constants.BINDING_ID, "Interact with the DIRIGERA binding.");
this.thingRegistry = thingRegistry;
}
@Override
public void execute(String[] args, Console console) {
DirigeraConsoleCommandDecoder decoder = new DirigeraConsoleCommandDecoder(args);
if (decoder.valid) {
switch (decoder.command) {
case CMD_TOKEN -> printToken(console);
case CMD_JSON -> printJSON(decoder, console);
case CMD_DEBUG -> setDebugParameters(decoder, console);
}
} else {
printUsage(console);
}
}
private void printToken(Console console) {
for (DirigeraHandler handler : getHubs()) {
console.println(handler.getThing().getLabel() + " token: " + handler.getToken());
}
}
private void printJSON(DirigeraConsoleCommandDecoder decodedCommand, Console console) {
String output = null;
if ("all".equals(decodedCommand.target)) {
for (DirigeraHandler handler : getHubs()) {
output = handler.getJSON();
}
} else {
for (DebugHandler handler : getDevices()) {
if (decodedCommand.target.equals(handler.getDeviceId())) {
output = handler.getJSON();
}
}
}
if (output != null) {
console.println(output);
} else {
console.println("Device Id " + decodedCommand.target + " not found");
}
}
private void setDebugParameters(DirigeraConsoleCommandDecoder decodedCommand, Console console) {
boolean success = false;
if ("all".equals(decodedCommand.target)) {
for (DirigeraHandler handler : getHubs()) {
handler.setDebug(decodedCommand.enable, true);
success = true;
}
} else {
for (DebugHandler handler : getDevices()) {
if (decodedCommand.target.equals(handler.getDeviceId())) {
handler.setDebug(decodedCommand.enable, false);
success = true;
}
}
}
if (success) {
console.println("Done");
} else {
console.println("Device Id " + decodedCommand.target + " not found");
}
}
private List<DirigeraHandler> getHubs() {
return thingRegistry.getAll().stream().map(thing -> thing.getHandler())
.filter(DirigeraHandler.class::isInstance).map(DirigeraHandler.class::cast).toList();
}
private List<DebugHandler> getDevices() {
return thingRegistry.getAll().stream().map(thing -> thing.getHandler()).filter(DebugHandler.class::isInstance)
.map(DebugHandler.class::cast).toList();
}
private List<String> getDeviceIds() {
return getDevices().stream().map(debugHandler -> debugHandler.getDeviceId()).toList();
}
@Override
public List<String> getUsages() {
return Arrays.asList(buildCommandUsage(CMD_TOKEN, "Get token from DIRIGERA hub"),
buildCommandUsage(CMD_JSON + " [<deviceId> | all]", "Print JSON data"),
buildCommandUsage(CMD_DEBUG + " [<deviceId> | all] [true | false] ",
"Enable / disable detailed debugging for specific / all devices"));
}
@Override
public @Nullable ConsoleCommandCompleter getCompleter() {
return new DirigeraConsoleCommandCompleter();
}
}

View File

@ -0,0 +1,52 @@
/*
* Copyright (c) 2010-2025 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.dirigera.internal.discovery;
import static org.openhab.binding.dirigera.internal.Constants.SUPPORTED_THING_TYPES_UIDS;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.config.discovery.AbstractDiscoveryService;
import org.openhab.core.config.discovery.DiscoveryResult;
import org.openhab.core.config.discovery.DiscoveryService;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
/**
* {@link DirigeraDiscoveryService} notifies about about devices found by
* DIRIGERA hub
*
* @author Bernd Weymann - Initial Contribution
*/
@NonNullByDefault
@Component(service = { DiscoveryService.class,
DirigeraDiscoveryService.class }, configurationPid = "dirigera.device.discovery")
public class DirigeraDiscoveryService extends AbstractDiscoveryService {
@Activate
public DirigeraDiscoveryService() {
super(SUPPORTED_THING_TYPES_UIDS, 90);
}
public void deviceDiscovered(DiscoveryResult result) {
thingDiscovered(result);
}
public void deviceRemoved(DiscoveryResult result) {
thingRemoved(result.getThingUID());
}
@Override
protected void startScan() {
// no manual scan supported
}
}

View File

@ -0,0 +1,108 @@
/*
* Copyright (c) 2010-2025 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.dirigera.internal.discovery;
import static org.openhab.binding.dirigera.internal.Constants.*;
import java.net.Inet4Address;
import java.net.InetAddress;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import javax.jmdns.ServiceInfo;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.dirigera.internal.Constants;
import org.openhab.core.config.discovery.DiscoveryResult;
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
import org.openhab.core.config.discovery.mdns.MDNSDiscoveryParticipant;
import org.openhab.core.thing.ThingRegistry;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.ThingUID;
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;
/**
* {@link DirigeraMDNSDiscoveryParticipant} for mDNS discovery of DIRIGERA gateway
*
* @author Bernd Weymann - Initial contribution
*/
@NonNullByDefault
@Component(configurationPid = "dirigera.mdns.discovery")
public class DirigeraMDNSDiscoveryParticipant implements MDNSDiscoveryParticipant {
private static final String SERVICE_TYPE = "_ihsp._tcp.local.";
private final Logger logger = LoggerFactory.getLogger(DirigeraMDNSDiscoveryParticipant.class);
protected final ThingRegistry thingRegistry;
@Activate
public DirigeraMDNSDiscoveryParticipant(final @Reference ThingRegistry thingRegistry) {
this.thingRegistry = thingRegistry;
}
@Override
public Set<ThingTypeUID> getSupportedThingTypeUIDs() {
return Set.of(Constants.THING_TYPE_GATEWAY);
}
@Override
public String getServiceType() {
return SERVICE_TYPE;
}
@Override
public @Nullable DiscoveryResult createResult(ServiceInfo si) {
logger.trace("DIRIGERA mDNS createResult for {} with IPs {}", si.getQualifiedNameMap(), si.getURLs());
Inet4Address[] ipAddresses = si.getInet4Addresses();
String gatewayName = si.getQualifiedNameMap().get(ServiceInfo.Fields.Instance);
if (gatewayName != null) {
String ipAddress = null;
if (ipAddresses.length == 0) {
// case of mDNS isn't delivering IP address try to resolve it
String domain = si.getQualifiedNameMap().get(ServiceInfo.Fields.Domain);
String gatewayHostName = gatewayName + "." + domain;
try {
InetAddress address = InetAddress.getByName(gatewayHostName);
ipAddress = address.getHostAddress();
} catch (Exception e) {
logger.warn("DIRIGERA mDNS failed to resolve IP for {} reason {}", gatewayHostName, e.getMessage());
}
} else if (ipAddresses.length > 0) {
ipAddress = ipAddresses[0].getHostAddress();
}
if (ipAddress != null) {
Map<String, Object> properties = new HashMap<>();
properties.put(PROPERTY_IP_ADDRESS, ipAddress);
return DiscoveryResultBuilder.create(new ThingUID(THING_TYPE_GATEWAY, gatewayName))
.withLabel("DIRIGERA Hub").withRepresentationProperty(PROPERTY_IP_ADDRESS)
.withProperties(properties).build();
}
}
return null;
}
@Override
public @Nullable ThingUID getThingUID(ServiceInfo si) {
String gatewayName = si.getQualifiedNameMap().get(ServiceInfo.Fields.Instance);
if (gatewayName != null) {
return new ThingUID(Constants.THING_TYPE_GATEWAY, gatewayName);
}
return null;
}
}

View File

@ -0,0 +1,30 @@
/*
* Copyright (c) 2010-2025 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.dirigera.internal.exception;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* {@link ApiException} thrown in case of problems accessing API
*
* @author Bernd Weymann - Initial contribution
*/
@NonNullByDefault
public class ApiException extends RuntimeException {
private static final long serialVersionUID = -9075334430125847975L;
public ApiException(String message) {
super(message);
}
}

View File

@ -0,0 +1,30 @@
/*
* Copyright (c) 2010-2025 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.dirigera.internal.exception;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* {@link ApiException} thrown in case of problems accessing DIRIGERA gateway
*
* @author Bernd Weymann - Initial contribution
*/
@NonNullByDefault
public class GatewayException extends RuntimeException {
private static final long serialVersionUID = -9187744844610930469L;
public GatewayException(String message) {
super(message);
}
}

View File

@ -0,0 +1,30 @@
/*
* Copyright (c) 2010-2025 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.dirigera.internal.exception;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* {@link ModelException} thrown in case of problems accessing Model
*
* @author Bernd Weymann - Initial contribution
*/
@NonNullByDefault
public class ModelException extends RuntimeException {
private static final long serialVersionUID = -3080953131870014248L;
public ModelException(String message) {
super(message);
}
}

View File

@ -0,0 +1,740 @@
/*
* Copyright (c) 2010-2025 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.dirigera.internal.handler;
import static org.openhab.binding.dirigera.internal.Constants.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.json.JSONArray;
import org.json.JSONObject;
import org.openhab.binding.dirigera.internal.config.BaseDeviceConfiguration;
import org.openhab.binding.dirigera.internal.exception.GatewayException;
import org.openhab.binding.dirigera.internal.interfaces.DebugHandler;
import org.openhab.binding.dirigera.internal.interfaces.Gateway;
import org.openhab.binding.dirigera.internal.interfaces.Model;
import org.openhab.binding.dirigera.internal.interfaces.PowerListener;
import org.openhab.core.library.CoreItemFactory;
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.library.unit.Units;
import org.openhab.core.thing.Bridge;
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.BaseThingHandler;
import org.openhab.core.thing.binding.BridgeHandler;
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.thing.type.ChannelTypeUID;
import org.openhab.core.types.Command;
import org.openhab.core.types.CommandOption;
import org.openhab.core.types.RefreshType;
import org.openhab.core.types.State;
import org.openhab.core.types.UnDefType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* {@link BaseHandler} for all devices
*
* @author Bernd Weymann - Initial contribution
*/
@NonNullByDefault
public class BaseHandler extends BaseThingHandler implements DebugHandler {
private final Logger logger = LoggerFactory.getLogger(BaseHandler.class);
private List<PowerListener> powerListeners = new ArrayList<>();
private @Nullable Gateway gateway;
// to be overwritten by child class in order to route the updates to the right instance
protected @Nullable BaseHandler child;
// maps to route properties to channels and vice versa
protected Map<String, String> property2ChannelMap;
protected Map<String, String> channel2PropertyMap;
// cache to handle each refresh command properly
protected Map<String, State> channelStateMap;
/*
* hardlinks initialized with invalid links because the first update shall trigger a link update. If it's declared
* as empty no link update will be triggered. This is necessary for startup phase.
*/
protected List<String> hardLinks = new ArrayList<>(Arrays.asList("undef"));
protected List<String> softLinks = new ArrayList<>();
protected List<String> linkCandidateTypes = new ArrayList<>();
/**
* Lists for canReceive and can Send capabilities
*/
protected List<String> receiveCapabilities = new ArrayList<>();
protected List<String> sendCapabilities = new ArrayList<>();
protected State requestedPowerState = UnDefType.UNDEF;
protected State currentPowerState = UnDefType.UNDEF;
protected BaseDeviceConfiguration config;
protected String customName = "";
protected String deviceType = "";
protected boolean disposed = true;
protected boolean online = false;
protected boolean customDebug = false;
public BaseHandler(Thing thing, Map<String, String> mapping) {
super(thing);
config = new BaseDeviceConfiguration();
// mapping contains, reverse mapping for commands plus state cache
property2ChannelMap = mapping;
channel2PropertyMap = reverse(mapping);
channelStateMap = initializeCache(mapping);
}
protected void setChildHandler(BaseHandler child) {
this.child = child;
}
@Override
public void initialize() {
disposed = false;
config = getConfigAs(BaseDeviceConfiguration.class);
// first get bridge as Gateway
Bridge bridge = getBridge();
if (bridge != null) {
updateStatus(ThingStatus.UNKNOWN);
BridgeHandler handler = bridge.getHandler();
if (handler != null) {
if (handler instanceof Gateway gw) {
gateway = gw;
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"@text/dirigera.device.status.wrong-bridge-type");
return;
}
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"@text/dirigera.device.missing-bridge-handler");
return;
}
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"@text/dirigera.device.status.missing-bridge");
return;
}
if (!checkHandler()) {
// if handler doesn't match model status will be set to offline and it will stay until correction
return;
}
if (!config.id.isBlank()) {
updateProperties();
BaseHandler proxy = child;
if (proxy != null) {
gateway().registerDevice(proxy, config.id);
}
}
}
private void updateProperties() {
// fill canSend and canReceive capabilities
Map<String, Object> modelProperties = gateway().model().getPropertiesFor(config.id);
Object canReceiveCapabilities = modelProperties.get(Model.PROPERTY_CAN_RECEIVE);
if (canReceiveCapabilities instanceof JSONArray jsonArray) {
jsonArray.forEach(capability -> {
if (!receiveCapabilities.contains(capability.toString())) {
receiveCapabilities.add(capability.toString());
}
});
}
Object canSendCapabilities = modelProperties.get(Model.PROPERTY_CAN_SEND);
if (canSendCapabilities instanceof JSONArray jsonArray) {
jsonArray.forEach(capability -> {
if (!sendCapabilities.contains(capability.toString())) {
sendCapabilities.add(capability.toString());
}
});
}
TreeMap<String, String> handlerProperties = new TreeMap<>(editProperties());
modelProperties.forEach((key, value) -> {
handlerProperties.put(key, value.toString());
});
updateProperties(handlerProperties);
}
/**
* Handling of basic commands which are the same for many devices
* - RefreshType for all channels
* - Startup behavior for lights and plugs
* - Power state for lights and plugs
*/
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
if (customDebug) {
logger.info("DIRIGERA {} handleCommand channel {} command {} {}", thing.getUID(), channelUID.getAsString(),
command.toFullString(), command.getClass());
}
if (command instanceof RefreshType) {
String channel = channelUID.getIdWithoutGroup();
State cachedState = channelStateMap.get(channel);
if (cachedState != null) {
super.updateState(channelUID, cachedState);
}
} else {
String targetChannel = channelUID.getIdWithoutGroup();
String targetProperty = channel2PropertyMap.get(targetChannel);
if (targetProperty != null) {
switch (targetChannel) {
case CHANNEL_STARTUP_BEHAVIOR:
if (command instanceof DecimalType decimal) {
String behaviorCommand = STARTUP_BEHAVIOR_REVERSE_MAPPING.get(decimal.intValue());
if (behaviorCommand != null) {
JSONObject stqartupAttributes = new JSONObject();
stqartupAttributes.put(targetProperty, behaviorCommand);
sendAttributes(stqartupAttributes);
}
break;
}
break;
case CHANNEL_POWER_STATE:
if (command instanceof OnOffType onOff) {
if (customDebug) {
logger.info("DIRIGERA BASE_HANDLER {} OnOff command: Current {} / Wanted {}",
thing.getLabel(), currentPowerState, onOff);
}
requestedPowerState = onOff;
if (!currentPowerState.equals(onOff)) {
JSONObject attributes = new JSONObject();
attributes.put(targetProperty, OnOffType.ON.equals(onOff));
sendAttributes(attributes);
} else {
requestedPowerState = UnDefType.UNDEF;
if (customDebug) {
logger.info("DIRIGERA BASE_HANDLER Dismiss {} {}", thing.getLabel(), onOff);
}
}
}
break;
case CHANNEL_CUSTOM_NAME:
if (command instanceof StringType string) {
JSONObject attributes = new JSONObject();
attributes.put(targetProperty, string.toString());
sendAttributes(attributes);
}
break;
}
} else {
// handle channels which are not defined in device map
switch (targetChannel) {
case CHANNEL_LINKS:
if (customDebug) {
logger.info("DIRIGERA BASE_HANDLER {} remove connection {}", thing.getLabel(),
command.toFullString());
}
if (command instanceof StringType string) {
linkUpdate(string.toFullString(), false);
}
break;
case CHANNEL_LINK_CANDIDATES:
if (customDebug) {
logger.info("DIRIGERA BASE_HANDLER {} add link {}", thing.getLabel(),
command.toFullString());
}
if (command instanceof StringType string) {
linkUpdate(string.toFullString(), true);
}
break;
}
}
}
}
/**
* Wrapper function to respect customDebug flag
*
* @param attributes
* @return status
*/
protected int sendAttributes(JSONObject attributes) {
int status = gateway().api().sendAttributes(config.id, attributes);
if (customDebug) {
logger.info("DIRIGERA BASE_HANDLER {} API call: Status {} payload {}", thing.getUID(), status, attributes);
}
return status;
}
/**
* Wrapper function to respect customDebug flag
*
* @param attributes
* @return status
*/
protected int sendPatch(JSONObject patch) {
int status = gateway().api().sendPatch(config.id, patch);
if (customDebug) {
logger.info("DIRIGERA BASE_HANDLER {} API call: Status {} payload {}", thing.getUID(), status, patch);
}
return status;
}
/**
* Handling generic channel updates for many devices.
* If they are not present in child configuration they won't be triggered.
* - Reachable flag for every device to evaluate ONLINE and OFFLINE states
* - Over the air (OTA) updates channels
* - Battery charge level
* - Startup behavior for lights and plugs
* - Power state for lights and plugs
* - custom name
*
* @param update
*/
public void handleUpdate(JSONObject update) {
if (customDebug) {
logger.info("DIRIGERA BASE_HANDLER {} handleUpdate JSON {}", thing.getUID(), update);
}
// check online offline for each device
if (update.has(Model.REACHABLE)) {
if (update.getBoolean(Model.REACHABLE)) {
updateStatus(ThingStatus.ONLINE);
online = true;
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"@text/dirigera.device.status.not-reachable");
online = false;
}
}
if (update.has(PROPERTY_DEVICE_TYPE) && deviceType.isBlank()) {
deviceType = update.getString(PROPERTY_DEVICE_TYPE);
}
if (update.has(Model.ATTRIBUTES)) {
JSONObject attributes = update.getJSONObject(Model.ATTRIBUTES);
// check OTA for each device
if (attributes.has(PROPERTY_OTA_STATUS)) {
createChannelIfNecessary(CHANNEL_OTA_STATUS, "ota-status", CoreItemFactory.NUMBER);
String otaStatusString = attributes.getString(PROPERTY_OTA_STATUS);
Integer otaStatus = OTA_STATUS_MAP.get(otaStatusString);
if (otaStatus != null) {
updateState(new ChannelUID(thing.getUID(), CHANNEL_OTA_STATUS), new DecimalType(otaStatus));
} else {
logger.warn("DIRIGERA BASE_HANDLER {} Cannot decode ota status {}", thing.getLabel(),
otaStatusString);
}
}
if (attributes.has(PROPERTY_OTA_STATE)) {
createChannelIfNecessary(CHANNEL_OTA_STATE, "ota-state", CoreItemFactory.NUMBER);
String otaStateString = attributes.getString(PROPERTY_OTA_STATE);
Integer otaState = OTA_STATE_MAP.get(otaStateString);
if (otaState != null) {
updateState(new ChannelUID(thing.getUID(), CHANNEL_OTA_STATE), new DecimalType(otaState));
// if ota state changes also update properties to keep firmware in thing properties up to date
updateProperties();
} else {
logger.warn("DIRIGERA BASE_HANDLER {} Cannot decode ota state {}", thing.getLabel(),
otaStateString);
}
}
if (attributes.has(PROPERTY_OTA_PROGRESS)) {
createChannelIfNecessary(CHANNEL_OTA_PROGRESS, "ota-percent", "Number:Dimensionless");
updateState(new ChannelUID(thing.getUID(), CHANNEL_OTA_PROGRESS),
QuantityType.valueOf(attributes.getInt(PROPERTY_OTA_PROGRESS), Units.PERCENT));
}
// battery also common, not for all but sensors and remote controller
if (attributes.has(PROPERTY_BATTERY_PERCENTAGE)) {
updateState(new ChannelUID(thing.getUID(), CHANNEL_BATTERY_LEVEL),
QuantityType.valueOf(attributes.getInt(PROPERTY_BATTERY_PERCENTAGE), Units.PERCENT));
}
if (attributes.has(PROPERTY_STARTUP_BEHAVIOR)) {
String startupString = attributes.getString(PROPERTY_STARTUP_BEHAVIOR);
Integer startupValue = STARTUP_BEHAVIOR_MAPPING.get(startupString);
if (startupValue != null) {
updateState(new ChannelUID(thing.getUID(), CHANNEL_STARTUP_BEHAVIOR),
new DecimalType(startupValue));
} else {
logger.warn("DIRIGERA BASE_HANDLER {} Cannot decode startup behavior {}", thing.getLabel(),
startupString);
}
}
if (attributes.has(PROPERTY_POWER_STATE)) {
currentPowerState = OnOffType.from(attributes.getBoolean(PROPERTY_POWER_STATE));
updateState(new ChannelUID(thing.getUID(), CHANNEL_POWER_STATE), currentPowerState);
synchronized (powerListeners) {
if (online) {
boolean requested = currentPowerState.equals(requestedPowerState);
powerListeners.forEach(listener -> {
listener.powerChanged((OnOffType) currentPowerState, requested);
});
requestedPowerState = UnDefType.UNDEF;
}
}
}
if (attributes.has(PROPERTY_CUSTOM_NAME) && customName.isBlank()) {
customName = attributes.getString(PROPERTY_CUSTOM_NAME);
updateState(new ChannelUID(thing.getUID(), CHANNEL_CUSTOM_NAME), StringType.valueOf(customName));
}
}
if (update.has(PROPERTY_REMOTE_LINKS)) {
JSONArray remoteLinks = update.getJSONArray(PROPERTY_REMOTE_LINKS);
List<String> updateList = new ArrayList<>();
remoteLinks.forEach(link -> {
updateList.add(link.toString());
});
Collections.sort(updateList);
Collections.sort(hardLinks);
if (!hardLinks.equals(updateList)) {
hardLinks = updateList;
// just update internal link list and let the gateway update do all updates regarding soft links
gateway().updateLinks();
}
}
}
protected synchronized void createChannelIfNecessary(String channelId, String channelTypeUID, String itemType) {
if (thing.getChannel(channelId) == null) {
if (customDebug) {
logger.info("DIRIGERA BASE_HANDLER {} create Channel {} {} {}", thing.getUID(), channelId,
channelTypeUID, itemType);
}
// https://www.openhab.org/docs/developer/bindings/#updating-the-thing-structure
ThingBuilder thingBuilder = editThing();
// channel type UID needs to be defined in channel-types.xml
Channel channel = ChannelBuilder.create(new ChannelUID(thing.getUID(), channelId), itemType)
.withType(new ChannelTypeUID(BINDING_ID, channelTypeUID)).build();
updateThing(thingBuilder.withChannel(channel).build());
}
}
protected boolean isPowered() {
return OnOffType.ON.equals(currentPowerState) && online;
}
/**
* Update cache for refresh, then update state
*/
@Override
protected void updateState(ChannelUID channelUID, State state) {
channelStateMap.put(channelUID.getIdWithoutGroup(), state);
if (!disposed) {
if (customDebug) {
logger.info("DIRIGERA {} updateState {} {}", thing.getUID(), channelUID, state);
}
super.updateState(channelUID, state);
}
}
@Override
public void dispose() {
disposed = true;
online = false;
BaseHandler proxy = child;
if (proxy != null) {
gateway().unregisterDevice(proxy, config.id);
}
super.dispose();
}
@Override
public void handleRemoval() {
BaseHandler proxy = child;
if (proxy != null) {
gateway().deleteDevice(proxy, config.id);
}
super.handleRemoval();
}
public Gateway gateway() {
Gateway gw = gateway;
if (gw != null) {
return gw;
} else {
throw new GatewayException(thing.getUID() + " has no Gateway defined");
}
}
protected boolean checkHandler() {
// cross check if configured thing type is matching with the model
// if handler is taken from discovery this will do no harm
// but if it's created manually mismatch can happen
ThingTypeUID modelTTUID = gateway().model().identifyDeviceFromModel(config.id);
if (!thing.getThingTypeUID().equals(modelTTUID)) {
// check if id is present in model
if (THING_TYPE_NOT_FOUND.equals(modelTTUID)) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.GONE,
"@text/dirigera.device.status.id-not-found" + " [\"" + config.id + "\"]");
} else {
// String message = "Handler " + thing.getThingTypeUID() + " doesn't match with model " + modelTTUID;
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"@text/dirigera.device.status.ttuid-mismatch" + " [\"" + thing.getThingTypeUID() + "\",\""
+ modelTTUID + "\"]");
}
return false;
}
return true;
}
private Map<String, State> initializeCache(Map<String, String> mapping) {
final Map<String, State> stateMap = new HashMap<>();
mapping.forEach((key, value) -> {
stateMap.put(key, UnDefType.UNDEF);
});
return stateMap;
}
/**
* Evaluates if this device is a controller or sensor
*
* @return boolean
*/
protected boolean isControllerOrSensor() {
return deviceType.toLowerCase().contains("sensor") || deviceType.toLowerCase().contains("controller");
}
/**
* Handling of links
*/
/**
* Update cycle of gateway is done
*/
public void updateLinksStart() {
softLinks.clear();
}
/**
* Get real links from device updates. Delivers a copy due to concurrent access.
*
* @return links attached to this device
*/
public List<String> getLinks() {
return new ArrayList<String>(hardLinks);
}
private void linkUpdate(String linkedDeviceId, boolean add) {
/**
* link has to be set to target device like light or outlet, not to the device which triggers an action like
* lightController or motionSensor
*/
String targetDevice = "";
String triggerDevice = "";
List<String> linksToSend = new ArrayList<>();
if (isControllerOrSensor()) {
// request needs to be sent to target device
targetDevice = linkedDeviceId;
triggerDevice = config.id;
// get current links
JSONObject deviceData = gateway().model().getAllFor(targetDevice, PROPERTY_DEVICES);
if (deviceData.has(PROPERTY_REMOTE_LINKS)) {
JSONArray jsonLinks = deviceData.getJSONArray(PROPERTY_REMOTE_LINKS);
jsonLinks.forEach(link -> {
linksToSend.add(link.toString());
});
if (customDebug) {
logger.info("DIRIGERA BASE_HANDLER {} links for {} {}", thing.getLabel(),
gateway().model().getCustonNameFor(targetDevice), linksToSend);
}
// this is sensor branch so add link of sensor
if (add) {
if (!linksToSend.contains(triggerDevice)) {
linksToSend.add(triggerDevice);
} else {
if (customDebug) {
logger.info("DIRIGERA BASE_HANDLER {} already linked {}", thing.getLabel(),
gateway().model().getCustonNameFor(triggerDevice));
}
}
} else {
if (linksToSend.contains(triggerDevice)) {
linksToSend.remove(triggerDevice);
} else {
if (customDebug) {
logger.info("DIRIGERA BASE_HANDLER {} no link to remove {}", thing.getLabel(),
gateway().model().getCustonNameFor(triggerDevice));
}
}
}
} else {
if (customDebug) {
logger.info("DIRIGERA BASE_HANDLER {} has no remoteLinks", thing.getLabel());
}
}
} else {
// send update to this device
targetDevice = config.id;
triggerDevice = linkedDeviceId;
if (add) {
hardLinks.add(triggerDevice);
} else {
hardLinks.remove(triggerDevice);
}
linksToSend.addAll(hardLinks);
}
JSONArray newLinks = new JSONArray(linksToSend);
JSONObject attributes = new JSONObject();
attributes.put(PROPERTY_REMOTE_LINKS, newLinks);
gateway().api().sendPatch(targetDevice, attributes);
// after api command remoteLinks property will be updated and trigger new linkUpadte
}
/**
* Adds a soft link towards the device which has the link stored in his attributes
*
* @param device id of the device which contains this link
*/
public void addSoftlink(String id) {
if (!softLinks.contains(id) && !config.id.equals(id)) {
softLinks.add(id);
}
}
/**
* Update cycle of gateway is done
*/
public void updateLinksDone() {
if (hasLinksOrCandidates()) {
createChannelIfNecessary(CHANNEL_LINKS, CHANNEL_LINKS, CoreItemFactory.STRING);
createChannelIfNecessary(CHANNEL_LINK_CANDIDATES, CHANNEL_LINK_CANDIDATES, CoreItemFactory.STRING);
updateLinks();
// The candidates needs to be evaluated by child class
// - blindController needs blinds and vice versa
// - soundCotroller needs speakers and vice versa
// - lightController needs light and outlet and vice versa
// So assure "linkCandidateTypes" are overwritten by child class with correct types
updateCandidateLinks();
}
}
protected void updateLinks() {
List<String> display = new ArrayList<>();
List<CommandOption> linkCommandOptions = new ArrayList<>();
List<String> allLinks = new ArrayList<>();
allLinks.addAll(hardLinks);
allLinks.addAll(softLinks);
Collections.sort(allLinks);
allLinks.forEach(link -> {
String customName = gateway().model().getCustonNameFor(link);
if (!gateway().isKnownDevice(link)) {
// if device isn't present in OH attach this suffix
customName += " (!)";
}
display.add(customName);
linkCommandOptions.add(new CommandOption(link, customName));
});
ChannelUID channelUUID = new ChannelUID(thing.getUID(), CHANNEL_LINKS);
gateway().getCommandProvider().setCommandOptions(channelUUID, linkCommandOptions);
logger.trace("DIRIGERA BASE_HANDLER {} links {}", thing.getLabel(), display);
updateState(channelUUID, StringType.valueOf(display.toString()));
}
protected void updateCandidateLinks() {
List<String> possibleCandidates = gateway().model().getDevicesForTypes(linkCandidateTypes);
List<String> candidates = new ArrayList<>();
possibleCandidates.forEach(entry -> {
if (!hardLinks.contains(entry) && !softLinks.contains(entry)) {
candidates.add(entry);
}
});
List<String> display = new ArrayList<>();
List<CommandOption> candidateOptions = new ArrayList<>();
Collections.sort(candidates);
candidates.forEach(candidate -> {
String customName = gateway().model().getCustonNameFor(candidate);
if (!gateway().isKnownDevice(candidate)) {
// if device isn't present in OH attach this suffix
customName += " (!)";
}
display.add(customName);
candidateOptions.add(new CommandOption(candidate, customName));
});
ChannelUID channelUUID = new ChannelUID(thing.getUID(), CHANNEL_LINK_CANDIDATES);
gateway().getCommandProvider().setCommandOptions(channelUUID, candidateOptions);
updateState(channelUUID, StringType.valueOf(display.toString()));
}
/**
* Check is any outgoing or incoming links or candidates are available
*
* @return true if one of the above conditions is true
*/
private boolean hasLinksOrCandidates() {
return (!hardLinks.isEmpty() || !softLinks.isEmpty()
|| !gateway().model().getDevicesForTypes(linkCandidateTypes).isEmpty());
}
public void addPowerListener(PowerListener listener) {
synchronized (powerListeners) {
powerListeners.add(listener);
}
}
public void removePowerListener(PowerListener listener) {
synchronized (powerListeners) {
powerListeners.remove(listener);
}
}
/**
* Debug commands for console access
*/
@Override
public String getJSON() {
if (THING_TYPE_SCENE.equals(thing.getThingTypeUID())) {
return gateway().api().readScene(config.id).toString();
} else {
return gateway().api().readDevice(config.id).toString();
}
}
@Override
public String getToken() {
return gateway().getToken();
}
@Override
public void setDebug(boolean debug, boolean all) {
if (all) {
((DebugHandler) gateway()).setDebug(debug, all);
} else {
customDebug = debug;
}
}
@Override
public String getDeviceId() {
return config.id;
}
/**
* for unit testing
*/
@Override
public @Nullable ThingHandlerCallback getCallback() {
return super.getCallback();
}
}

View File

@ -0,0 +1,61 @@
/*
* Copyright (c) 2010-2025 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.dirigera.internal.handler;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* The {@link DeviceUpdate} element handled in device update queue
*
* @author Bernd Weymann - Initial contribution
*/
@NonNullByDefault
public class DeviceUpdate {
public enum Action {
ADD,
DISPOSE,
REMOVE,
LINKS;
}
public @Nullable BaseHandler handler;
public String deviceId;
public Action action;
public DeviceUpdate(@Nullable BaseHandler handler, String deviceId, Action action) {
this.handler = handler;
this.deviceId = deviceId;
this.action = action;
}
/**
* Link updates are equal because they are generic, all others false
*
* @param other
* @return
*/
@Override
public boolean equals(@Nullable Object other) {
boolean result = false;
if (other instanceof DeviceUpdate otherDeviceUpdate) {
result = this.action.equals(otherDeviceUpdate.action) && this.deviceId.equals(otherDeviceUpdate.deviceId);
BaseHandler thisProxyHandler = this.handler;
BaseHandler otherProxyHandler = otherDeviceUpdate.handler;
if (result && thisProxyHandler != null && otherProxyHandler != null) {
result = thisProxyHandler.equals(otherProxyHandler);
}
}
return result;
}
}

View File

@ -0,0 +1,162 @@
/*
* Copyright (c) 2010-2025 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.dirigera.internal.handler.airpurifier;
import static org.openhab.binding.dirigera.internal.Constants.*;
import java.util.Iterator;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.json.JSONObject;
import org.openhab.binding.dirigera.internal.handler.BaseHandler;
import org.openhab.binding.dirigera.internal.interfaces.Model;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.PercentType;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.unit.Units;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.types.Command;
import org.openhab.core.types.State;
/**
* The {@link AirPurifierHandler} for handling air cleaning devices
*
* @author Bernd Weymann - Initial contribution
*/
@NonNullByDefault
public class AirPurifierHandler extends BaseHandler {
/**
* see
* https://github.com/dvdgeisler/DirigeraClient/blob/a760b4419a8b1adf469d14a6ce4e750e52d4d540/dirigera-client-api/src/main/java/de/dvdgeisler/iot/dirigera/client/api/model/device/airpurifier/AirPurifierFanMode.java#L5
**/
public static final Map<String, Integer> FAN_MODES = Map.of("auto", 0, "low", 1, "medium", 2, "high", 3, "on", 4,
"off", 5);
/**
* see
* https://github.com/Leggin/dirigera/blob/790a3151d8b61151dcd31f2194297dc8d4d89640/src/dirigera/devices/air_purifier.py#L61
**/
public static final int FAN_SPEED_MAX = 50;
public static Map<Integer, String> fanModeToState = reverseStateMapping(FAN_MODES);
public AirPurifierHandler(Thing thing, Map<String, String> mapping) {
super(thing, mapping);
super.setChildHandler(this);
}
@Override
public void initialize() {
super.initialize();
if (super.checkHandler()) {
JSONObject values = gateway().api().readDevice(config.id);
handleUpdate(values);
}
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
super.handleCommand(channelUID, command);
String channel = channelUID.getIdWithoutGroup();
String targetProperty = channel2PropertyMap.get(channel);
if (targetProperty != null) {
switch (channel) {
case CHANNEL_CHILD_LOCK:
case CHANNEL_DISABLE_STATUS_LIGHT:
if (command instanceof OnOffType onOff) {
JSONObject onOffAttributes = new JSONObject();
onOffAttributes.put(targetProperty, OnOffType.ON.equals(onOff));
super.sendAttributes(onOffAttributes);
}
break;
case CHANNEL_PURIFIER_FAN_SPEED:
if (command instanceof PercentType percent) {
long speedAbs = Math.round(percent.intValue() * FAN_SPEED_MAX / 100.0);
JSONObject fanSpeedAttributes = new JSONObject();
fanSpeedAttributes.put(targetProperty, speedAbs);
super.sendAttributes(fanSpeedAttributes);
}
break;
case CHANNEL_PURIFIER_FAN_MODE:
if (command instanceof DecimalType decimal) {
int fanMode = decimal.intValue();
String fanModeAttribute = fanModeToState.get(fanMode);
if (fanModeAttribute != null) {
JSONObject fanModeAttributes = new JSONObject();
fanModeAttributes.put(targetProperty, fanModeAttribute);
super.sendAttributes(fanModeAttributes);
}
}
break;
}
}
}
@Override
public void handleUpdate(JSONObject update) {
super.handleUpdate(update);
if (update.has(Model.ATTRIBUTES)) {
JSONObject attributes = update.getJSONObject(Model.ATTRIBUTES);
Iterator<String> attributesIterator = attributes.keys();
while (attributesIterator.hasNext()) {
String key = attributesIterator.next();
String targetChannel = property2ChannelMap.get(key);
if (targetChannel != null) {
switch (targetChannel) {
case CHANNEL_PURIFIER_FAN_MODE:
String fanMode = attributes.getString(key);
Integer fanModeNumber = FAN_MODES.get(fanMode);
if (fanModeNumber != null) {
updateState(new ChannelUID(thing.getUID(), targetChannel),
new DecimalType(fanModeNumber));
}
break;
case CHANNEL_PURIFIER_FAN_SPEED:
float speed = attributes.getFloat(key);
speed = Math.max(Math.min(speed, FAN_SPEED_MAX), 0);
int percent = Math.round(speed * 100 / FAN_SPEED_MAX);
updateState(new ChannelUID(thing.getUID(), targetChannel), new PercentType(percent));
break;
case CHANNEL_PURIFIER_FAN_RUNTIME:
case CHANNEL_PURIFIER_FILTER_LIFETIME:
updateState(new ChannelUID(thing.getUID(), targetChannel),
QuantityType.valueOf(attributes.getDouble(key), Units.MINUTE));
break;
case CHANNEL_PURIFIER_FILTER_ELAPSED:
updateState(new ChannelUID(thing.getUID(), targetChannel),
QuantityType.valueOf(attributes.getDouble(key), Units.MINUTE));
State lifeTimeState = channelStateMap.get(CHANNEL_PURIFIER_FILTER_LIFETIME);
if (lifeTimeState != null && lifeTimeState instanceof QuantityType) {
int elapsed = attributes.getInt(key);
int lifetime = ((QuantityType<?>) lifeTimeState).intValue();
updateState(new ChannelUID(thing.getUID(), CHANNEL_PURIFIER_FILTER_REMAIN),
QuantityType.valueOf(lifetime - elapsed, Units.MINUTE));
}
break;
case CHANNEL_PARTICULATE_MATTER:
updateState(new ChannelUID(thing.getUID(), targetChannel),
QuantityType.valueOf(attributes.getDouble(key), Units.MICROGRAM_PER_CUBICMETRE));
break;
case CHANNEL_PURIFIER_FILTER_ALARM:
case CHANNEL_CHILD_LOCK:
case CHANNEL_DISABLE_STATUS_LIGHT:
updateState(new ChannelUID(thing.getUID(), targetChannel),
OnOffType.from(attributes.getBoolean(key)));
}
}
}
}
}
}

View File

@ -0,0 +1,125 @@
/*
* Copyright (c) 2010-2025 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.dirigera.internal.handler.blind;
import static org.openhab.binding.dirigera.internal.Constants.*;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.json.JSONObject;
import org.openhab.binding.dirigera.internal.handler.BaseHandler;
import org.openhab.binding.dirigera.internal.interfaces.Model;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.PercentType;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link BlindHandler} for Window / Door blinds
*
* @author Bernd Weymann - Initial contribution
*/
@NonNullByDefault
public class BlindHandler extends BaseHandler {
private final Logger logger = LoggerFactory.getLogger(BlindHandler.class);
public static final Map<String, Integer> BLIND_STATES = Map.of("stopped", 0, "up", 1, "down", 2);
public static Map<Integer, String> blindNumberToState = reverseStateMapping(BLIND_STATES);
public BlindHandler(Thing thing, Map<String, String> mapping) {
super(thing, mapping);
super.setChildHandler(this);
// links of types which can be established towards this device
linkCandidateTypes = List.of(DEVICE_TYPE_BLIND_CONTROLLER);
}
@Override
public void initialize() {
super.initialize();
if (super.checkHandler()) {
JSONObject values = gateway().api().readDevice(config.id);
handleUpdate(values);
}
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
String channel = channelUID.getIdWithoutGroup();
if (command instanceof RefreshType) {
super.handleCommand(channelUID, command);
} else {
String targetProperty = channel2PropertyMap.get(channel);
if (targetProperty != null) {
switch (channel) {
case CHANNEL_BLIND_STATE:
if (command instanceof DecimalType state) {
String commandAttribute = blindNumberToState.get(state.intValue());
if (commandAttribute != null) {
JSONObject attributes = new JSONObject();
attributes.put(targetProperty, commandAttribute);
super.sendAttributes(attributes);
} else {
logger.warn("DIRIGERA BLIND_DEVICE Blind state unknown {}", state.intValue());
}
}
break;
case CHANNEL_BLIND_LEVEL:
if (command instanceof PercentType percent) {
JSONObject attributes = new JSONObject();
attributes.put("blindsTargetLevel", percent.intValue());
super.sendAttributes(attributes);
}
break;
}
}
}
}
@Override
public void handleUpdate(JSONObject update) {
// handle reachable flag
super.handleUpdate(update);
// now device specific
if (update.has(Model.ATTRIBUTES)) {
JSONObject attributes = update.getJSONObject(Model.ATTRIBUTES);
Iterator<String> attributesIterator = attributes.keys();
while (attributesIterator.hasNext()) {
String key = attributesIterator.next();
String targetChannel = property2ChannelMap.get(key);
if (targetChannel != null) {
switch (targetChannel) {
case CHANNEL_BLIND_STATE:
String blindState = attributes.getString(key);
Integer stateValue = BLIND_STATES.get(blindState);
if (stateValue != null) {
updateState(new ChannelUID(thing.getUID(), targetChannel), new DecimalType(stateValue));
} else {
logger.warn("DIRIGERA BLIND_DEVICE Blind state unknown {}", blindState);
}
break;
case CHANNEL_BLIND_LEVEL:
updateState(new ChannelUID(thing.getUID(), targetChannel),
new PercentType(attributes.getInt(key)));
break;
}
}
}
}
}
}

View File

@ -0,0 +1,175 @@
/*
* Copyright (c) 2010-2025 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.dirigera.internal.handler.controller;
import static org.openhab.binding.dirigera.internal.Constants.PROPERTY_DEVICE_ID;
import java.time.Instant;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.json.JSONArray;
import org.json.JSONObject;
import org.openhab.binding.dirigera.internal.handler.BaseHandler;
import org.openhab.core.storage.Storage;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.types.Command;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link BaseShortcutController} for triggering scenes
*
* @author Bernd Weymann - Initial contribution
*/
@NonNullByDefault
public class BaseShortcutController extends BaseHandler {
private final Logger logger = LoggerFactory.getLogger(BaseShortcutController.class);
private Storage<String> storage;
public Map<String, String> sceneMapping = new HashMap<>();
private Map<String, Instant> triggerTimes = new HashMap<>();
private static final String SINGLE_PRESS = "singlePress";
private static final String DOUBLE_PRESS = "doublePress";
private static final String LONG_PRESS = "longPress";
private static final List<String> CLICK_PATTERNS = List.of(SINGLE_PRESS, DOUBLE_PRESS, LONG_PRESS);
public BaseShortcutController(Thing thing, Map<String, String> mapping, Storage<String> bindingStorage) {
super(thing, mapping);
super.setChildHandler(this);
this.storage = bindingStorage;
}
public void initializeScenes(String deviceId, String channel) {
// check scenes
CLICK_PATTERNS.forEach(pattern -> {
String patternKey = deviceId + ":" + channel + ":" + pattern;
if (!sceneMapping.containsKey(patternKey)) {
String patternSceneId = storage.get(patternKey);
if (patternSceneId != null) {
sceneMapping.put(patternKey, patternSceneId);
} else {
String uuid = getUID();
String createdUUID = gateway().api().createScene(uuid, pattern, deviceId);
if (uuid.equals(createdUUID)) {
storage.put(patternKey, createdUUID);
sceneMapping.put(patternKey, createdUUID);
} else {
logger.warn("DIRIGERA BASE_SHORTCUT_CONTROLLER scene create failed for {}", patternKey);
}
}
}
// after all check if scene is created and register for updates
String sceneId = sceneMapping.get(patternKey);
if (sceneId != null) {
gateway().registerDevice(this, sceneId);
}
});
}
@Override
public void dispose() {
sceneMapping.forEach((key, value) -> {
BaseHandler proxy = child;
if (proxy != null) {
gateway().unregisterDevice(proxy, value);
}
});
super.dispose();
}
@Override
public void handleRemoval() {
sceneMapping.forEach((key, value) -> {
// cleanup storage and hub
BaseHandler proxy = child;
if (proxy != null) {
gateway().deleteDevice(proxy, value);
}
gateway().api().deleteScene(value);
storage.remove(key);
});
super.handleRemoval();
}
private String getUID() {
String uuid = UUID.randomUUID().toString();
while (gateway().model().has(uuid)) {
uuid = UUID.randomUUID().toString();
}
return uuid;
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
super.handleCommand(channelUID, command);
}
@Override
public void handleUpdate(JSONObject update) {
super.handleUpdate(update);
if (update.has(PROPERTY_DEVICE_ID) && update.has("triggers")) {
// first check if trigger happened
String sceneId = update.getString(PROPERTY_DEVICE_ID);
JSONArray triggers = update.getJSONArray("triggers");
boolean triggered = false;
for (int i = 0; i < triggers.length(); i++) {
JSONObject triggerObject = triggers.getJSONObject(i);
if (triggerObject.has("triggeredAt")) {
String triggerTimeString = triggerObject.getString("triggeredAt");
Instant triggerTime = Instant.parse(triggerTimeString);
Instant lastTriggered = triggerTimes.get(sceneId);
if (lastTriggered != null) {
if (triggerTime.isAfter(lastTriggered)) {
triggerTimes.put(sceneId, triggerTime);
triggered = true;
}
} else {
triggered = true;
triggerTimes.put(sceneId, triggerTime);
break;
}
}
}
// if triggered deliver
if (triggered) {
sceneMapping.forEach((key, value) -> {
if (sceneId.equals(value)) {
String[] channelPattern = key.split(":");
String pattern = "";
switch (channelPattern[2]) {
case SINGLE_PRESS:
pattern = "SHORT_PRESSED";
break;
case DOUBLE_PRESS:
pattern = "DOUBLE_PRESSED";
break;
case LONG_PRESS:
pattern = "LONG_PRESSED";
break;
}
if (!pattern.isBlank()) {
triggerChannel(new ChannelUID(thing.getUID(), channelPattern[1]), pattern);
}
}
});
}
}
}
}

View File

@ -0,0 +1,61 @@
/*
* Copyright (c) 2010-2025 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.dirigera.internal.handler.controller;
import static org.openhab.binding.dirigera.internal.Constants.DEVICE_TYPE_BLINDS;
import java.util.List;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.json.JSONObject;
import org.openhab.binding.dirigera.internal.handler.BaseHandler;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.types.Command;
/**
* The {@link BlindsControllerHandler} basic DeviceHandler for all devices
*
* @author Bernd Weymann - Initial contribution
*/
@NonNullByDefault
public class BlindsControllerHandler extends BaseHandler {
public BlindsControllerHandler(Thing thing, Map<String, String> mapping) {
super(thing, mapping);
super.setChildHandler(this);
// links of types which can be established towards this device
linkCandidateTypes = List.of(DEVICE_TYPE_BLINDS);
}
@Override
public void initialize() {
super.initialize();
if (super.checkHandler()) {
JSONObject values = gateway().api().readDevice(config.id);
handleUpdate(values);
}
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
super.handleCommand(channelUID, command);
}
@Override
public void handleUpdate(JSONObject update) {
// handle reachable flag, no more special handling
super.handleUpdate(update);
}
}

View File

@ -0,0 +1,85 @@
/*
* Copyright (c) 2010-2025 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.dirigera.internal.handler.controller;
import static org.openhab.binding.dirigera.internal.Constants.*;
import java.util.Map;
import java.util.Map.Entry;
import java.util.TreeMap;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.json.JSONObject;
import org.openhab.core.storage.Storage;
import org.openhab.core.thing.Thing;
/**
* The {@link DoubleShortcutControllerHandler} for triggering scenes
*
* @author Bernd Weymann - Initial contribution
*/
@NonNullByDefault
public class DoubleShortcutControllerHandler extends BaseShortcutController {
public TreeMap<String, String> relations = new TreeMap<>();
public DoubleShortcutControllerHandler(Thing thing, Map<String, String> mapping, Storage<String> bindingStorage) {
super(thing, mapping, bindingStorage);
super.setChildHandler(this);
}
@Override
public void initialize() {
super.initialize();
if (super.checkHandler()) {
JSONObject values = gateway().api().readDevice(config.id);
handleUpdate(values);
// now register at gateway all device and scene ids
String relationId = gateway().model().getRelationId(config.id);
relations = gateway().model().getRelations(relationId);
Entry<String, String> firstEntry = relations.firstEntry();
String firstDeviceId = firstEntry.getKey();
super.initializeScenes(firstDeviceId, CHANNEL_BUTTON_1);
gateway().registerDevice(this, firstDeviceId);
values = gateway().api().readDevice(firstDeviceId);
handleUpdate(values);
// double shortcut controller has 2 devices
Entry<String, String> secondEntry = relations.higherEntry(firstEntry.getKey());
String secondDeviceId = secondEntry.getKey();
super.initializeScenes(secondDeviceId, CHANNEL_BUTTON_2);
gateway().registerDevice(this, secondDeviceId);
values = gateway().api().readDevice(secondDeviceId);
handleUpdate(values);
}
}
@Override
public void dispose() {
// remove device mapping
relations.forEach((key, value) -> {
gateway().unregisterDevice(this, key);
});
// super removes scene mapping
super.dispose();
}
@Override
public void handleRemoval() {
// delete device mapping
relations.forEach((key, value) -> {
gateway().deleteDevice(this, key);
});
// super deletes scenes from model
super.handleRemoval();
}
}

View File

@ -0,0 +1,114 @@
/*
* Copyright (c) 2010-2025 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.dirigera.internal.handler.controller;
import static org.openhab.binding.dirigera.internal.Constants.*;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.json.JSONArray;
import org.json.JSONObject;
import org.openhab.binding.dirigera.internal.handler.BaseHandler;
import org.openhab.binding.dirigera.internal.interfaces.Model;
import org.openhab.core.library.types.StringType;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.types.Command;
/**
* The {@link LightControllerHandler} basic DeviceHandler for all devices
*
* @author Bernd Weymann - Initial contribution
*/
@NonNullByDefault
public class LightControllerHandler extends BaseHandler {
public LightControllerHandler(Thing thing, Map<String, String> mapping) {
super(thing, mapping);
super.setChildHandler(this);
// links of types which can be established towards this device
linkCandidateTypes = List.of(DEVICE_TYPE_LIGHT, DEVICE_TYPE_OUTLET);
}
@Override
public void initialize() {
super.initialize();
if (super.checkHandler()) {
JSONObject values = gateway().api().readDevice(config.id);
handleUpdate(values);
}
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
super.handleCommand(channelUID, command);
String targetChannel = channelUID.getIdWithoutGroup();
switch (targetChannel) {
case CHANNEL_LIGHT_PRESET:
if (command instanceof StringType string) {
JSONArray presetValues = new JSONArray();
// handle the standard presets from IKEA app, custom otherwise without consistency check
switch (string.toFullString()) {
case "Off":
// fine - array stays empty
break;
case "Warm":
presetValues = new JSONArray(
gateway().model().getTemplate(Model.TEMPLATE_LIGHT_PRESET_WARM));
break;
case "Slowdown":
presetValues = new JSONArray(
gateway().model().getTemplate(Model.TEMPLATE_LIGHT_PRESET_SLOWDOWN));
break;
case "Smooth":
presetValues = new JSONArray(
gateway().model().getTemplate(Model.TEMPLATE_LIGHT_PRESET_SMOOTH));
break;
case "Bright":
presetValues = new JSONArray(
gateway().model().getTemplate(Model.TEMPLATE_LIGHT_PRESET_BRIGHT));
break;
default:
presetValues = new JSONArray(string.toFullString());
}
JSONObject preset = new JSONObject();
preset.put("circadianPresets", presetValues);
super.sendAttributes(preset);
}
}
}
@Override
public void handleUpdate(JSONObject update) {
super.handleUpdate(update);
if (update.has(Model.ATTRIBUTES)) {
JSONObject attributes = update.getJSONObject(Model.ATTRIBUTES);
Iterator<String> attributesIterator = attributes.keys();
while (attributesIterator.hasNext()) {
String key = attributesIterator.next();
switch (key) {
case "circadianPresets":
if (attributes.has("circadianPresets")) {
JSONArray lightPresets = attributes.getJSONArray("circadianPresets");
updateState(new ChannelUID(thing.getUID(), CHANNEL_LIGHT_PRESET),
StringType.valueOf(lightPresets.toString()));
}
break;
}
}
}
}
}

View File

@ -0,0 +1,46 @@
/*
* Copyright (c) 2010-2025 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.dirigera.internal.handler.controller;
import static org.openhab.binding.dirigera.internal.Constants.CHANNEL_BUTTON_1;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.json.JSONObject;
import org.openhab.core.storage.Storage;
import org.openhab.core.thing.Thing;
/**
* The {@link ShortcutControllerHandler} for triggering scenes
*
* @author Bernd Weymann - Initial contribution
*/
@NonNullByDefault
public class ShortcutControllerHandler extends BaseShortcutController {
public ShortcutControllerHandler(Thing thing, Map<String, String> mapping, Storage<String> bindingStorage) {
super(thing, mapping, bindingStorage);
super.setChildHandler(this);
}
@Override
public void initialize() {
super.initialize();
if (super.checkHandler()) {
JSONObject values = gateway().api().readDevice(config.id);
handleUpdate(values);
super.initializeScenes(config.id, CHANNEL_BUTTON_1);
}
}
}

View File

@ -0,0 +1,48 @@
/*
* Copyright (c) 2010-2025 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.dirigera.internal.handler.controller;
import static org.openhab.binding.dirigera.internal.Constants.DEVICE_TYPE_SPEAKER;
import java.util.List;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.json.JSONObject;
import org.openhab.binding.dirigera.internal.handler.BaseHandler;
import org.openhab.core.thing.Thing;
/**
* The {@link SoundControllerHandler} for controlling SYMFONSIK speakers
*
* @author Bernd Weymann - Initial contribution
*/
@NonNullByDefault
public class SoundControllerHandler extends BaseHandler {
public SoundControllerHandler(Thing thing, Map<String, String> mapping) {
super(thing, mapping);
super.setChildHandler(this);
// links of types which can be established towards this device
linkCandidateTypes = List.of(DEVICE_TYPE_SPEAKER);
}
@Override
public void initialize() {
super.initialize();
if (super.checkHandler()) {
JSONObject values = gateway().api().readDevice(config.id);
handleUpdate(values);
}
}
}

View File

@ -0,0 +1,237 @@
/*
* Copyright (c) 2010-2025 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.dirigera.internal.handler.light;
import static org.openhab.binding.dirigera.internal.Constants.*;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.json.JSONObject;
import org.openhab.binding.dirigera.internal.config.ColorLightConfiguration;
import org.openhab.binding.dirigera.internal.handler.BaseHandler;
import org.openhab.binding.dirigera.internal.interfaces.PowerListener;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.types.Command;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* {@link BaseLight} for handling light commands in a controlled way
*
* @author Bernd Weymann - Initial contribution
*/
@NonNullByDefault
public class BaseLight extends BaseHandler implements PowerListener {
private final Logger logger = LoggerFactory.getLogger(BaseLight.class);
protected ColorLightConfiguration lightConfig = new ColorLightConfiguration();
protected Map<LightCommand.Action, LightCommand> lastUserMode = new HashMap<>();
private List<LightCommand> lightRequestQueue = new ArrayList<>();
private Instant readyForNextCommand = Instant.now();
private JSONObject placeHolder = new JSONObject();
private boolean executingCommand = false;
public BaseLight(Thing thing, Map<String, String> mapping) {
super(thing, mapping);
super.setChildHandler(this);
// links of types which can be established towards this device
linkCandidateTypes = List.of(DEVICE_TYPE_LIGHT_CONTROLLER, DEVICE_TYPE_MOTION_SENSOR);
}
@Override
public void initialize() {
super.initialize();
lightConfig = getConfigAs(ColorLightConfiguration.class);
super.addPowerListener(this);
}
@Override
public void dispose() {
super.removePowerListener(this);
super.dispose();
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
String channel = channelUID.getIdWithoutGroup();
if (CHANNEL_POWER_STATE.equals(channel) && (command instanceof OnOffType onOff)) {
// route power state into queue instead of direct switch on / off
addOnOffCommand(OnOffType.ON.equals(onOff));
} else {
super.handleCommand(channelUID, command);
}
}
protected void addOnOffCommand(boolean on) {
LightCommand command;
if (on) {
command = new LightCommand(placeHolder, LightCommand.Action.ON);
} else {
command = new LightCommand(placeHolder, LightCommand.Action.OFF);
}
synchronized (lightRequestQueue) {
lightRequestQueue.add(command);
if (customDebug) {
logger.info("DIRIGERA BASE_LIGHT {} add command {}", thing.getLabel(), command.toString());
}
}
scheduler.execute(this::executeCommand);
}
protected void addCommand(@Nullable LightCommand command) {
if (command == null) {
return;
}
synchronized (lightRequestQueue) {
lightRequestQueue.add(command);
if (customDebug) {
logger.info("DIRIGERA BASE_LIGHT {} add command {}", thing.getLabel(), command.toString());
}
}
scheduler.execute(this::executeCommand);
}
/**
* execute commands in the order and delays of the lightRequestQueue
*/
protected void executeCommand() {
LightCommand request = null;
synchronized (lightRequestQueue) {
if (lightRequestQueue.isEmpty()) {
return;
}
// wait for next time window and previous command is fully executed
while (readyForNextCommand.isAfter(Instant.now()) || executingCommand) {
try {
lightRequestQueue.wait(50);
} catch (InterruptedException e) {
lightRequestQueue.clear();
Thread.interrupted();
return;
}
}
/*
* get command from queue and check if it needs to be executed
* if several requests of the same kind e.g. 5 brightness requests are in only the last one shall be
* executed
*/
if (!lightRequestQueue.isEmpty()) {
request = lightRequestQueue.remove(0);
} else {
lightRequestQueue.notifyAll();
return;
}
if (lightRequestQueue.contains(request)) {
lightRequestQueue.notifyAll();
return;
}
// now execute command
executingCommand = true;
}
if (customDebug) {
logger.info("DIRIGERA BASE_LIGHT {} execute {}", thing.getLabel(), request);
}
int addonMillis = 0;
switch (request.action) {
case ON:
super.handleCommand(new ChannelUID(thing.getUID(), CHANNEL_POWER_STATE), OnOffType.ON);
addonMillis = lightConfig.fadeTime;
break;
case OFF:
super.handleCommand(new ChannelUID(thing.getUID(), CHANNEL_POWER_STATE), OnOffType.OFF);
break;
case BRIGHTNESS:
case TEMPERATURE:
case COLOR:
super.sendAttributes(request.request);
if (isPowered()) {
addonMillis = lightConfig.fadeTime;
}
break;
}
// after command is sent to API add the time
readyForNextCommand = Instant.now().plus(addonMillis, ChronoUnit.MILLIS);
synchronized (lightRequestQueue) {
executingCommand = false;
lightRequestQueue.notifyAll();
}
}
protected void changeProperty(LightCommand.Action action, JSONObject request) {
LightCommand requestedCommand = new LightCommand(request, action);
if (isPowered()) {
addCommand(requestedCommand);
} else {
lastUserMode.put(action, requestedCommand);
switch (action) {
case COLOR:
addCommand(requestedCommand);
lastUserMode.remove(LightCommand.Action.TEMPERATURE);
break;
case TEMPERATURE:
addCommand(requestedCommand);
lastUserMode.remove(LightCommand.Action.COLOR);
break;
case BRIGHTNESS:
case ON:
case OFF:
default:
break;
}
logger.trace("DIRIGERA BASE_LIGHT {} last user mode settings {}", thing.getLabel(), lastUserMode);
}
}
@Override
public void powerChanged(OnOffType power, boolean requested) {
// apply lum settings according to configuration in the right sequence if power changed to ON
if (OnOffType.ON.equals(power)) {
if (!requested) {
addOnOffCommand(true);
}
if (customDebug) {
logger.info("DIRIGERA BASE_LIGHT {} last user mode restore {}", thing.getLabel(), lastUserMode);
}
LightCommand brightnessCommand = lastUserMode.remove(LightCommand.Action.BRIGHTNESS);
LightCommand colorCommand = lastUserMode.remove(LightCommand.Action.COLOR);
LightCommand temperatureCommand = lastUserMode.remove(LightCommand.Action.TEMPERATURE);
switch (lightConfig.fadeSequence) {
case 0:
addCommand(brightnessCommand);
addCommand(colorCommand);
addCommand(temperatureCommand);
break;
case 1:
addCommand(colorCommand);
addCommand(temperatureCommand);
addCommand(brightnessCommand);
break;
}
} else {
// assure settings are clean for next startup
lastUserMode.clear();
}
}
}

View File

@ -0,0 +1,213 @@
/*
* Copyright (c) 2010-2025 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.dirigera.internal.handler.light;
import static org.openhab.binding.dirigera.internal.Constants.*;
import java.util.Iterator;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.json.JSONObject;
import org.openhab.binding.dirigera.internal.DirigeraStateDescriptionProvider;
import org.openhab.binding.dirigera.internal.interfaces.Model;
import org.openhab.binding.dirigera.internal.model.ColorModel;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.HSBType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.PercentType;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.unit.Units;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.types.Command;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* {@link ColorLightHandler} for lights with hue, saturation and brightness
*
* @author Bernd Weymann - Initial contribution
*/
@NonNullByDefault
public class ColorLightHandler extends TemperatureLightHandler {
private final Logger logger = LoggerFactory.getLogger(ColorLightHandler.class);
private HSBType hsbStateReflection = new HSBType(); // proxy to reflect state to end user
private HSBType hsbDevice = new HSBType(); // strictly holding values which were received via update
private String colorMode = "";
public ColorLightHandler(Thing thing, Map<String, String> mapping, DirigeraStateDescriptionProvider stateProvider) {
super(thing, mapping, stateProvider);
super.setChildHandler(this);
}
@Override
public void initialize() {
super.initialize();
if (super.checkHandler()) {
JSONObject values = gateway().api().readDevice(config.id);
handleUpdate(values);
}
}
@Override
public void dispose() {
super.dispose();
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
super.handleCommand(channelUID, command);
String channel = channelUID.getIdWithoutGroup();
if (CHANNEL_LIGHT_COLOR.equals(channel)) {
if (command instanceof HSBType hsb) {
// respect sequence
switch (lightConfig.fadeSequence) {
case 0:
brightnessCommand(hsb);
colorCommand(hsb);
break;
case 1:
colorCommand(hsb);
brightnessCommand(hsb);
break;
}
hsbStateReflection = hsb;
updateState(channelUID, hsb);
} else if (command instanceof OnOffType) {
super.addOnOffCommand(OnOffType.ON.equals(command));
} else if (command instanceof PercentType percent) {
int requestedBrightness = percent.intValue();
if (requestedBrightness == 0) {
super.addOnOffCommand(false);
} else {
brightnessCommand(new HSBType("0,0," + requestedBrightness));
super.addOnOffCommand(true);
}
}
}
if (CHANNEL_LIGHT_TEMPERATURE.equals(channel) || CHANNEL_LIGHT_TEMPERATURE_ABS.equals(channel)) {
long kelvin = -1;
HSBType colorTemp = null;
if (command instanceof PercentType percent) {
kelvin = super.getKelvin(percent.intValue());
colorTemp = ColorModel.kelvin2Hsb(kelvin);
} else if (command instanceof QuantityType number) {
kelvin = number.intValue();
colorTemp = ColorModel.kelvin2Hsb(kelvin);
}
// there are color lights which cannot handle tempera HSB {}t ,kelvin,colorTempure as stored in capabilities
// in this case calculate color which is fitting to temperature
if (colorTemp != null && !receiveCapabilities.contains(Model.COLOR_TEMPERATURE_CAPABILITY)) {
HSBType colorTempAdaption = new HSBType(colorTemp.getHue(), colorTemp.getSaturation(),
hsbDevice.getBrightness());
if (customDebug) {
logger.info("DIRIGERA COLOR_LIGHT {} handle temperature as color {}", thing.getLabel(),
colorTempAdaption);
}
colorCommand(colorTempAdaption);
}
}
}
/**
* Send hue and saturation to light device in case of difference is more than 2%
*
* @param hsb as requested color
* @return true if color request is sent, false otherwise
*/
private void colorCommand(HSBType hsb) {
if (!"color".equals(colorMode) || !ColorModel.closeTo(hsb, hsbDevice, 0.02)) {
JSONObject colorAttributes = new JSONObject();
colorAttributes.put("colorHue", hsb.getHue().intValue());
colorAttributes.put("colorSaturation", hsb.getSaturation().intValue() / 100.0);
super.changeProperty(LightCommand.Action.COLOR, colorAttributes);
}
}
private void brightnessCommand(HSBType hsb) {
int requestedBrightness = hsb.getBrightness().intValue();
int currentBrightness = hsbDevice.getBrightness().intValue();
if (Math.abs(requestedBrightness - currentBrightness) > 1) {
if (requestedBrightness > 0) {
JSONObject brightnessattributes = new JSONObject();
brightnessattributes.put("lightLevel", hsb.getBrightness().intValue());
super.changeProperty(LightCommand.Action.BRIGHTNESS, brightnessattributes);
}
}
}
@Override
public void handleUpdate(JSONObject update) {
super.handleUpdate(update);
if (update.has(Model.ATTRIBUTES)) {
JSONObject attributes = update.getJSONObject(Model.ATTRIBUTES);
Iterator<String> attributesIterator = attributes.keys();
boolean deliverHSB = false;
while (attributesIterator.hasNext()) {
String key = attributesIterator.next();
if (ATTRIBUTE_COLOR_MODE.equals(key)) {
colorMode = attributes.getString(key);
}
String targetChannel = property2ChannelMap.get(key);
if (targetChannel != null) {
// apply and update to hsbCurrent, only in case !isOn deliver fake brightness HSBs
switch (targetChannel) {
case CHANNEL_LIGHT_COLOR:
switch (key) {
case "colorHue":
double hueValue = attributes.getInt(key);
hsbDevice = new HSBType(new DecimalType(hueValue), hsbDevice.getSaturation(),
hsbDevice.getBrightness());
hsbStateReflection = new HSBType(new DecimalType(hueValue),
hsbStateReflection.getSaturation(), hsbStateReflection.getBrightness());
deliverHSB = true;
break;
case "colorSaturation":
int saturationValue = Math.round(attributes.getFloat(key) * 100);
hsbDevice = new HSBType(hsbDevice.getHue(), new PercentType(saturationValue),
hsbDevice.getBrightness());
hsbStateReflection = new HSBType(hsbStateReflection.getHue(),
new PercentType(saturationValue), hsbStateReflection.getBrightness());
deliverHSB = true;
break;
case "lightLevel":
int brightnessValue = attributes.getInt(key);
// device needs the right values
hsbDevice = new HSBType(hsbDevice.getHue(), hsbDevice.getSaturation(),
new PercentType(brightnessValue));
hsbStateReflection = new HSBType(hsbStateReflection.getHue(),
hsbStateReflection.getSaturation(), new PercentType(brightnessValue));
deliverHSB = true;
break;
}
break;
}
}
}
if (deliverHSB) {
updateState(new ChannelUID(thing.getUID(), CHANNEL_LIGHT_COLOR), hsbStateReflection);
if (!receiveCapabilities.contains(Model.COLOR_TEMPERATURE_CAPABILITY)) {
// if color light doesn't support native light temperature converted values are taken
long kelvin = Math.min(colorTemperatureMin,
Math.max(colorTemperatureMax, ColorModel.hsb2Kelvin(hsbStateReflection)));
updateState(new ChannelUID(thing.getUID(), CHANNEL_LIGHT_TEMPERATURE),
new PercentType(getPercent(kelvin)));
updateState(new ChannelUID(thing.getUID(), CHANNEL_LIGHT_TEMPERATURE_ABS),
QuantityType.valueOf(kelvin, Units.KELVIN));
}
}
}
}
}

View File

@ -0,0 +1,118 @@
/*
* Copyright (c) 2010-2025 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.dirigera.internal.handler.light;
import static org.openhab.binding.dirigera.internal.Constants.*;
import java.util.Iterator;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.json.JSONObject;
import org.openhab.binding.dirigera.internal.interfaces.Model;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.PercentType;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.types.Command;
/**
* {@link DimmableLightHandler} for lights with brightness
*
* @author Bernd Weymann - Initial contribution
*/
@NonNullByDefault
public class DimmableLightHandler extends BaseLight {
protected int currentBrightness = 0;
public DimmableLightHandler(Thing thing, Map<String, String> mapping) {
super(thing, mapping);
super.setChildHandler(this);
}
@Override
public void initialize() {
super.initialize();
if (super.checkHandler()) {
JSONObject values = gateway().api().readDevice(config.id);
handleUpdate(values);
}
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
super.handleCommand(channelUID, command);
String channel = channelUID.getIdWithoutGroup();
String targetProperty = channel2PropertyMap.get(channel);
if (targetProperty != null) {
switch (channel) {
case CHANNEL_LIGHT_BRIGHTNESS:
if (command instanceof PercentType percent) {
int percentValue = percent.intValue();
// switch on or off depending on brightness ...
if (percentValue > 0) {
// first change brightness to be stored for power ON ...
if (Math.abs(percentValue - currentBrightness) > 1) {
JSONObject brightnessAttributes = new JSONObject();
brightnessAttributes.put(targetProperty, percent.intValue());
super.changeProperty(LightCommand.Action.BRIGHTNESS, brightnessAttributes);
}
// .. then switch power
if (!isPowered()) {
super.addOnOffCommand(true);
}
} else {
super.addOnOffCommand(false);
}
} else if (command instanceof OnOffType onOff) {
super.addOnOffCommand(OnOffType.ON.equals(onOff));
}
break;
}
}
}
@Override
public void handleUpdate(JSONObject update) {
super.handleUpdate(update);
if (update.has(Model.ATTRIBUTES)) {
JSONObject attributes = update.getJSONObject(Model.ATTRIBUTES);
Iterator<String> attributesIterator = attributes.keys();
while (attributesIterator.hasNext()) {
String key = attributesIterator.next();
String targetChannel = property2ChannelMap.get(key);
if (targetChannel != null) {
switch (targetChannel) {
case CHANNEL_LIGHT_BRIGHTNESS:
// set new currentBrightness as received and continue with update depending on power state
currentBrightness = attributes.getInt(key);
case CHANNEL_POWER_STATE:
/**
* Power state changed
* on - report last received brightness
* off - deliver brightness 0
*/
if (isPowered()) {
updateState(new ChannelUID(thing.getUID(), CHANNEL_LIGHT_BRIGHTNESS),
new PercentType(currentBrightness));
} else {
updateState(new ChannelUID(thing.getUID(), CHANNEL_LIGHT_BRIGHTNESS),
new PercentType(0));
}
break;
}
}
}
}
}
}

View File

@ -0,0 +1,57 @@
/*
* Copyright (c) 2010-2025 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.dirigera.internal.handler.light;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.json.JSONObject;
/**
* The {@link LightCommand} is holding all information to execute a new light command
*
* @author Bernd Weymann - Initial contribution
*/
@NonNullByDefault
public class LightCommand {
public enum Action {
ON,
BRIGHTNESS,
TEMPERATURE,
COLOR,
OFF
}
public JSONObject request;
public Action action;
public LightCommand(JSONObject request, Action action) {
this.request = request;
this.action = action;
}
/**
* Link updates are equal because they are generic, all others false
*
* @param other
* @return
*/
@Override
public boolean equals(@Nullable Object other) {
return (other instanceof LightCommand command && action.equals(command.action));
}
@Override
public String toString() {
return this.action + ": " + this.request.toString();
}
}

View File

@ -0,0 +1,42 @@
/*
* Copyright (c) 2010-2025 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.dirigera.internal.handler.light;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.json.JSONObject;
import org.openhab.core.thing.Thing;
/**
* {@link SwitchLightHandler} for lights which can only be switched on / off
*
* @author Bernd Weymann - Initial contribution
*/
@NonNullByDefault
public class SwitchLightHandler extends BaseLight {
public SwitchLightHandler(Thing thing, Map<String, String> mapping) {
super(thing, mapping);
super.setChildHandler(this);
}
@Override
public void initialize() {
super.initialize();
if (super.checkHandler()) {
JSONObject values = gateway().api().readDevice(config.id);
handleUpdate(values);
}
}
}

View File

@ -0,0 +1,164 @@
/*
* Copyright (c) 2010-2025 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.dirigera.internal.handler.light;
import static org.openhab.binding.dirigera.internal.Constants.*;
import java.math.BigDecimal;
import java.util.Iterator;
import java.util.Map;
import java.util.TreeMap;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.json.JSONObject;
import org.openhab.binding.dirigera.internal.DirigeraStateDescriptionProvider;
import org.openhab.binding.dirigera.internal.interfaces.Model;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.PercentType;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.unit.Units;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.types.Command;
import org.openhab.core.types.StateDescriptionFragment;
import org.openhab.core.types.StateDescriptionFragmentBuilder;
/**
* {@link TemperatureLightHandler} for lights with brightness and color temperature
*
* @author Bernd Weymann - Initial contribution
*/
@NonNullByDefault
public class TemperatureLightHandler extends DimmableLightHandler {
private PercentType currentColorTemp = new PercentType();
protected final DirigeraStateDescriptionProvider stateProvider;
// default values of "standard IKEA lamps" from JSON
protected int colorTemperatureMin = 4000;
protected int colorTemperatureMax = 2202;
protected int range = colorTemperatureMin - colorTemperatureMax;
public TemperatureLightHandler(Thing thing, Map<String, String> mapping,
DirigeraStateDescriptionProvider stateProvider) {
super(thing, mapping);
super.setChildHandler(this);
this.stateProvider = stateProvider;
}
@Override
public void initialize() {
super.initialize();
if (super.checkHandler()) {
JSONObject values = gateway().api().readDevice(config.id);
JSONObject attributes = values.getJSONObject(Model.ATTRIBUTES);
// check for settings of color temperature in attributes
TreeMap<String, String> properties = new TreeMap<>(editProperties());
Iterator<String> attributesIterator = attributes.keys();
while (attributesIterator.hasNext()) {
String key = attributesIterator.next();
if ("colorTemperatureMin".equals(key)) {
colorTemperatureMin = attributes.getInt(key);
properties.put("colorTemperatureMin", String.valueOf(colorTemperatureMin));
} else if ("colorTemperatureMax".equals(key)) {
colorTemperatureMax = attributes.getInt(key);
properties.put("colorTemperatureMax", String.valueOf(colorTemperatureMax));
}
}
StateDescriptionFragment fragment = StateDescriptionFragmentBuilder.create()
.withMinimum(BigDecimal.valueOf(colorTemperatureMax))
.withMaximum(BigDecimal.valueOf(colorTemperatureMin)).withStep(BigDecimal.valueOf(100))
.withPattern("%.0f K").withReadOnly(false).build();
stateProvider.setStateDescription(new ChannelUID(thing.getUID(), CHANNEL_LIGHT_TEMPERATURE_ABS), fragment);
updateProperties(properties);
range = colorTemperatureMin - colorTemperatureMax;
handleUpdate(values);
}
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
super.handleCommand(channelUID, command);
String channel = channelUID.getIdWithoutGroup();
String targetProperty = channel2PropertyMap.get(channel);
switch (channel) {
case CHANNEL_LIGHT_TEMPERATURE_ABS:
targetProperty = "colorTemperature";
case CHANNEL_LIGHT_TEMPERATURE:
long kelvinValue = -1;
int percentValue = -1;
if (command instanceof PercentType percent) {
percentValue = percent.intValue();
kelvinValue = getKelvin(percent.intValue());
} else if (command instanceof QuantityType number) {
kelvinValue = number.intValue();
percentValue = getPercent(kelvinValue);
} else if (command instanceof OnOffType onOff) {
super.addOnOffCommand(OnOffType.ON.equals(onOff));
}
/*
* some color lights which inherit this temperature light don't have the temperature capability.
* As workaround child class ColorLightHandler is handling color temperature
*/
if (receiveCapabilities.contains(Model.COLOR_TEMPERATURE_CAPABILITY) && percentValue != -1
&& kelvinValue != -1) {
JSONObject attributes = new JSONObject();
attributes.put(targetProperty, kelvinValue);
super.changeProperty(LightCommand.Action.TEMPERATURE, attributes);
if (!isPowered()) {
// fake event for power OFF
updateState(new ChannelUID(thing.getUID(), CHANNEL_LIGHT_TEMPERATURE),
new PercentType(percentValue));
updateState(new ChannelUID(thing.getUID(), CHANNEL_LIGHT_TEMPERATURE_ABS),
QuantityType.valueOf(kelvinValue, Units.KELVIN));
}
}
}
}
@Override
public void handleUpdate(JSONObject update) {
super.handleUpdate(update);
if (update.has(Model.ATTRIBUTES)) {
JSONObject attributes = update.getJSONObject(Model.ATTRIBUTES);
Iterator<String> attributesIterator = attributes.keys();
while (attributesIterator.hasNext()) {
String key = attributesIterator.next();
String targetChannel = property2ChannelMap.get(key);
if (targetChannel != null) {
switch (targetChannel) {
case CHANNEL_LIGHT_TEMPERATURE:
int kelvin = attributes.getInt(key);
// seems some lamps are delivering temperature values out of range
// keep it in range with min/max
kelvin = Math.min(kelvin, colorTemperatureMin);
kelvin = Math.max(kelvin, colorTemperatureMax);
int percent = getPercent(kelvin);
currentColorTemp = new PercentType(percent);
updateState(new ChannelUID(thing.getUID(), targetChannel), currentColorTemp);
updateState(new ChannelUID(thing.getUID(), CHANNEL_LIGHT_TEMPERATURE_ABS),
QuantityType.valueOf(kelvin, Units.KELVIN));
break;
}
}
}
}
}
protected long getKelvin(int percent) {
return Math.round(colorTemperatureMin - (range * percent / 100));
}
protected int getPercent(long kelvin) {
return Math.min(100, Math.max(0, Math.round(100 - ((kelvin - colorTemperatureMax) * 100 / range))));
}
}

View File

@ -0,0 +1,88 @@
/*
* Copyright (c) 2010-2025 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.dirigera.internal.handler.plug;
import static org.openhab.binding.dirigera.internal.Constants.*;
import java.util.Iterator;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.json.JSONObject;
import org.openhab.binding.dirigera.internal.interfaces.Model;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.types.Command;
/**
* The {@link PowerPlugHandler} basic DeviceHandler for all devices
*
* @author Bernd Weymann - Initial contribution
*/
@NonNullByDefault
public class PowerPlugHandler extends SimplePlugHandler {
public PowerPlugHandler(Thing thing, Map<String, String> mapping) {
super(thing, mapping);
super.setChildHandler(this);
}
@Override
public void initialize() {
super.initialize();
// update of values is handled in super class
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
super.handleCommand(channelUID, command);
String channel = channelUID.getIdWithoutGroup();
String targetProperty = channel2PropertyMap.get(channel);
if (targetProperty != null) {
switch (channel) {
case CHANNEL_CHILD_LOCK:
case CHANNEL_DISABLE_STATUS_LIGHT:
if (command instanceof OnOffType onOff) {
JSONObject attributes = new JSONObject();
attributes.put(targetProperty, OnOffType.ON.equals(onOff));
super.sendAttributes(attributes);
}
break;
}
}
}
@Override
public void handleUpdate(JSONObject update) {
// handle reachable flag
super.handleUpdate(update);
// now device specific
if (update.has(Model.ATTRIBUTES)) {
JSONObject attributes = update.getJSONObject(Model.ATTRIBUTES);
Iterator<String> attributesIterator = attributes.keys();
while (attributesIterator.hasNext()) {
String key = attributesIterator.next();
String targetChannel = property2ChannelMap.get(key);
if (targetChannel != null) {
switch (targetChannel) {
case CHANNEL_CHILD_LOCK:
case CHANNEL_DISABLE_STATUS_LIGHT:
updateState(new ChannelUID(thing.getUID(), targetChannel),
OnOffType.from(attributes.getBoolean(key)));
break;
}
}
}
}
}
}

View File

@ -0,0 +1,48 @@
/*
* Copyright (c) 2010-2025 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.dirigera.internal.handler.plug;
import static org.openhab.binding.dirigera.internal.Constants.*;
import java.util.List;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.json.JSONObject;
import org.openhab.binding.dirigera.internal.handler.BaseHandler;
import org.openhab.core.thing.Thing;
/**
* The {@link SimplePlugHandler} basic DeviceHandler for all devices
*
* @author Bernd Weymann - Initial contribution
*/
@NonNullByDefault
public class SimplePlugHandler extends BaseHandler {
public SimplePlugHandler(Thing thing, Map<String, String> mapping) {
super(thing, mapping);
super.setChildHandler(this);
// links of types which can be established towards this device
linkCandidateTypes = List.of(DEVICE_TYPE_LIGHT_CONTROLLER, DEVICE_TYPE_MOTION_SENSOR);
}
@Override
public void initialize() {
super.initialize();
if (super.checkHandler()) {
// handling of first update, also for PowerPlug and SmartPlug child classes!
JSONObject values = gateway().api().readDevice(config.id);
handleUpdate(values);
}
}
}

View File

@ -0,0 +1,124 @@
/*
* Copyright (c) 2010-2025 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.dirigera.internal.handler.plug;
import static org.openhab.binding.dirigera.internal.Constants.*;
import java.time.Instant;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.json.JSONObject;
import org.openhab.binding.dirigera.internal.interfaces.Model;
import org.openhab.core.library.types.DateTimeType;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.unit.Units;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.types.Command;
/**
* The {@link SmartPlugHandler} basic DeviceHandler for all devices
*
* @author Bernd Weymann - Initial contribution
*/
@NonNullByDefault
public class SmartPlugHandler extends PowerPlugHandler {
private double totalEnergy = -1;
private double resetEnergy = -1;
public SmartPlugHandler(Thing thing, Map<String, String> mapping) {
super(thing, mapping);
super.setChildHandler(this);
}
@Override
public void initialize() {
super.initialize();
// update of values is handled in super class
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
super.handleCommand(channelUID, command);
String channel = channelUID.getIdWithoutGroup();
switch (channel) {
case CHANNEL_ENERGY_RESET_DATE:
if (command instanceof DateTimeType) {
scheduler.schedule(this::energyReset, 250, TimeUnit.MILLISECONDS);
}
}
}
private void energyReset() {
JSONObject reset = new JSONObject("{\"energyConsumedAtLastReset\": 0}");
super.sendAttributes(reset);
}
@Override
public void handleUpdate(JSONObject update) {
// handle reachable flag
super.handleUpdate(update);
// now device specific
if (update.has(Model.ATTRIBUTES)) {
JSONObject attributes = update.getJSONObject(Model.ATTRIBUTES);
Iterator<String> attributesIterator = attributes.keys();
while (attributesIterator.hasNext()) {
String key = attributesIterator.next();
String targetChannel = property2ChannelMap.get(key);
if (targetChannel != null) {
switch (targetChannel) {
case CHANNEL_POWER:
updateState(new ChannelUID(thing.getUID(), targetChannel),
QuantityType.valueOf(attributes.getDouble(key), Units.WATT));
break;
case CHANNEL_CURRENT:
updateState(new ChannelUID(thing.getUID(), targetChannel),
QuantityType.valueOf(attributes.getDouble(key), Units.AMPERE));
break;
case CHANNEL_POTENTIAL:
updateState(new ChannelUID(thing.getUID(), targetChannel),
QuantityType.valueOf(attributes.getDouble(key), Units.VOLT));
break;
case CHANNEL_ENERGY_TOTAL:
totalEnergy = attributes.getDouble(key);
updateState(new ChannelUID(thing.getUID(), CHANNEL_ENERGY_TOTAL),
QuantityType.valueOf(totalEnergy, Units.KILOWATT_HOUR));
if (totalEnergy >= 0 && resetEnergy >= 0) {
double diff = totalEnergy - resetEnergy;
updateState(new ChannelUID(thing.getUID(), CHANNEL_ENERGY_RESET),
QuantityType.valueOf(diff, Units.KILOWATT_HOUR));
}
break;
case CHANNEL_ENERGY_RESET:
resetEnergy = attributes.getDouble(key);
if (totalEnergy >= 0 && resetEnergy >= 0) {
double diff = totalEnergy - resetEnergy;
updateState(new ChannelUID(thing.getUID(), CHANNEL_ENERGY_RESET),
QuantityType.valueOf(diff, Units.KILOWATT_HOUR));
}
break;
case CHANNEL_ENERGY_RESET_DATE:
String dateTime = attributes.getString(key);
Instant restTime = Instant.parse(dateTime);
updateState(new ChannelUID(thing.getUID(), CHANNEL_ENERGY_RESET_DATE),
new DateTimeType(restTime));
break;
}
}
}
}
}
}

View File

@ -0,0 +1,56 @@
/*
* Copyright (c) 2010-2025 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.dirigera.internal.handler.repeater;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.json.JSONObject;
import org.openhab.binding.dirigera.internal.handler.BaseHandler;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.types.Command;
/**
* The {@link RepeaterHandler} basic DeviceHandler for all devices
*
* @author Bernd Weymann - Initial contribution
*/
@NonNullByDefault
public class RepeaterHandler extends BaseHandler {
public RepeaterHandler(Thing thing, Map<String, String> mapping) {
super(thing, mapping);
super.setChildHandler(this);
}
@Override
public void initialize() {
super.initialize();
if (super.checkHandler()) {
JSONObject values = gateway().api().readDevice(config.id);
handleUpdate(values);
}
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
super.handleCommand(channelUID, command);
}
@Override
public void handleUpdate(JSONObject update) {
// handle reachable flag, no more special handling
super.handleUpdate(update);
}
}

View File

@ -0,0 +1,115 @@
/*
* Copyright (c) 2010-2025 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.dirigera.internal.handler.scene;
import static org.openhab.binding.dirigera.internal.Constants.*;
import java.time.Duration;
import java.time.Instant;
import java.util.Arrays;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.json.JSONObject;
import org.openhab.binding.dirigera.internal.handler.BaseHandler;
import org.openhab.core.library.types.DateTimeType;
import org.openhab.core.library.types.DecimalType;
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.types.Command;
import org.openhab.core.types.UnDefType;
/**
* The {@link SceneHandler} for triggering defined scenes
*
* @author Bernd Weymann - Initial contribution
*/
@NonNullByDefault
public class SceneHandler extends BaseHandler {
private Instant lastTrigger = Instant.MAX;
private int undoDuration = 30;
public SceneHandler(Thing thing, Map<String, String> mapping) {
super(thing, mapping);
super.setChildHandler(this);
// no link support for Scenes
hardLinks = Arrays.asList();
}
@Override
public void initialize() {
super.initialize();
if (super.checkHandler()) {
JSONObject values = gateway().api().readScene(config.id);
handleUpdate(values);
if (values.isEmpty()) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.GONE,
"@text/dirigera.scene.status.scene-not-found");
} else {
updateStatus(ThingStatus.ONLINE);
}
// check if different undo duration is configured
if (values.has("undoAllowedDuration")) {
undoDuration = values.getInt("undoAllowedDuration");
}
}
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
super.handleCommand(channelUID, command);
if (CHANNEL_TRIGGER.equals(channelUID.getIdWithoutGroup())) {
if (command instanceof DecimalType decimal) {
int commandNumber = decimal.intValue();
switch (commandNumber) {
case 0:
gateway().api().triggerScene(config.id, "trigger");
lastTrigger = Instant.now();
scheduler.schedule(this::countDown, 1, TimeUnit.SECONDS);
break;
case 1:
gateway().api().triggerScene(config.id, "undo");
lastTrigger = Instant.MAX;
updateState(new ChannelUID(thing.getUID(), CHANNEL_TRIGGER), UnDefType.UNDEF);
break;
}
}
}
}
@Override
public void handleUpdate(JSONObject update) {
super.handleUpdate(update);
if (update.has("lastTriggered")) {
Instant lastRiggeredInstant = Instant.parse(update.getString("lastTriggered"));
DateTimeType dtt = new DateTimeType(lastRiggeredInstant);
updateState(new ChannelUID(thing.getUID(), CHANNEL_LAST_TRIGGER), dtt);
}
}
private void countDown() {
long seconds = Duration.between(lastTrigger, Instant.now()).toSeconds();
if (seconds >= 0 && seconds <= 30) {
long countDown = undoDuration - seconds;
updateState(new ChannelUID(thing.getUID(), CHANNEL_TRIGGER), new DecimalType(countDown));
scheduler.schedule(this::countDown, 1, TimeUnit.SECONDS);
} else {
updateState(new ChannelUID(thing.getUID(), CHANNEL_TRIGGER), UnDefType.UNDEF);
}
}
}

View File

@ -0,0 +1,97 @@
/*
* Copyright (c) 2010-2025 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.dirigera.internal.handler.sensor;
import static org.openhab.binding.dirigera.internal.Constants.*;
import java.util.Arrays;
import java.util.Iterator;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.json.JSONObject;
import org.openhab.binding.dirigera.internal.handler.BaseHandler;
import org.openhab.binding.dirigera.internal.interfaces.Model;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.unit.SIUnits;
import org.openhab.core.library.unit.Units;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.types.Command;
/**
* The {@link AirQualityHandler} basic DeviceHandler for all devices
*
* @author Bernd Weymann - Initial contribution
*/
@NonNullByDefault
public class AirQualityHandler extends BaseHandler {
public AirQualityHandler(Thing thing, Map<String, String> mapping) {
super(thing, mapping);
super.setChildHandler(this);
// no link support for Scenes
hardLinks = Arrays.asList();
}
@Override
public void initialize() {
super.initialize();
if (super.checkHandler()) {
JSONObject values = gateway().api().readDevice(config.id);
handleUpdate(values);
}
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
super.handleCommand(channelUID, command);
}
@Override
public void handleUpdate(JSONObject update) {
// handle reachable flag
super.handleUpdate(update);
// now device specific
if (update.has(Model.ATTRIBUTES)) {
JSONObject attributes = update.getJSONObject(Model.ATTRIBUTES);
Iterator<String> attributesIterator = attributes.keys();
while (attributesIterator.hasNext()) {
String key = attributesIterator.next();
String targetChannel = property2ChannelMap.get(key);
if (targetChannel != null) {
switch (targetChannel) {
case CHANNEL_TEMPERATURE:
double temperature = Math.round(attributes.getDouble(key) * 10) / 10.0;
updateState(new ChannelUID(thing.getUID(), CHANNEL_TEMPERATURE),
QuantityType.valueOf(temperature, SIUnits.CELSIUS));
break;
case CHANNEL_HUMIDITY:
updateState(new ChannelUID(thing.getUID(), CHANNEL_HUMIDITY),
QuantityType.valueOf(attributes.getDouble(key), Units.PERCENT));
break;
case CHANNEL_PARTICULATE_MATTER:
updateState(new ChannelUID(thing.getUID(), CHANNEL_PARTICULATE_MATTER),
QuantityType.valueOf(attributes.getDouble(key), Units.MICROGRAM_PER_CUBICMETRE));
break;
case CHANNEL_VOC_INDEX:
updateState(new ChannelUID(thing.getUID(), CHANNEL_VOC_INDEX),
new DecimalType(attributes.getDouble(key)));
break;
}
}
}
}
}
}

View File

@ -0,0 +1,84 @@
/*
* Copyright (c) 2010-2025 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.dirigera.internal.handler.sensor;
import static org.openhab.binding.dirigera.internal.Constants.CHANNEL_CONTACT;
import java.util.Arrays;
import java.util.Iterator;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.json.JSONObject;
import org.openhab.binding.dirigera.internal.handler.BaseHandler;
import org.openhab.binding.dirigera.internal.interfaces.Model;
import org.openhab.core.library.types.OpenClosedType;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.types.Command;
/**
* The {@link ContactSensorHandler} basic DeviceHandler for all devices
*
* @author Bernd Weymann - Initial contribution
*/
@NonNullByDefault
public class ContactSensorHandler extends BaseHandler {
public ContactSensorHandler(Thing thing, Map<String, String> mapping) {
super(thing, mapping);
super.setChildHandler(this);
// no link support for Scenes
hardLinks = Arrays.asList();
}
@Override
public void initialize() {
super.initialize();
if (super.checkHandler()) {
JSONObject values = gateway().api().readDevice(config.id);
handleUpdate(values);
}
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
super.handleCommand(channelUID, command);
}
@Override
public void handleUpdate(JSONObject update) {
// handle reachable flag
super.handleUpdate(update);
// now device specific
if (update.has(Model.ATTRIBUTES)) {
JSONObject attributes = update.getJSONObject(Model.ATTRIBUTES);
Iterator<String> attributesIterator = attributes.keys();
while (attributesIterator.hasNext()) {
String key = attributesIterator.next();
String targetChannel = property2ChannelMap.get(key);
if (targetChannel != null) {
switch (targetChannel) {
case CHANNEL_CONTACT:
OpenClosedType state = OpenClosedType.CLOSED;
if (attributes.getBoolean(key)) {
state = OpenClosedType.OPEN;
}
updateState(new ChannelUID(thing.getUID(), targetChannel), state);
break;
}
}
}
}
}
}

View File

@ -0,0 +1,76 @@
/*
* Copyright (c) 2010-2025 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.dirigera.internal.handler.sensor;
import static org.openhab.binding.dirigera.internal.Constants.CHANNEL_ILLUMINANCE;
import java.util.Iterator;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.json.JSONObject;
import org.openhab.binding.dirigera.internal.handler.BaseHandler;
import org.openhab.binding.dirigera.internal.interfaces.Model;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.unit.Units;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.types.Command;
/**
* The {@link LightSensorHandler} basic DeviceHandler for all devices
*
* @author Bernd Weymann - Initial contribution
*/
@NonNullByDefault
public class LightSensorHandler extends BaseHandler {
public LightSensorHandler(Thing thing, Map<String, String> mapping) {
super(thing, mapping);
super.setChildHandler(this);
}
@Override
public void initialize() {
super.initialize();
if (super.checkHandler()) {
JSONObject values = gateway().api().readDevice(config.id);
handleUpdate(values);
}
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
super.handleCommand(channelUID, command);
}
@Override
public void handleUpdate(JSONObject update) {
// handle reachable flag
super.handleUpdate(update);
// now device specific
if (update.has(Model.ATTRIBUTES)) {
JSONObject attributes = update.getJSONObject(Model.ATTRIBUTES);
Iterator<String> attributesIterator = attributes.keys();
while (attributesIterator.hasNext()) {
String key = attributesIterator.next();
String targetChannel = property2ChannelMap.get(key);
if (targetChannel != null) {
if (CHANNEL_ILLUMINANCE.equals(targetChannel)) {
updateState(new ChannelUID(thing.getUID(), targetChannel),
QuantityType.valueOf(attributes.getInt(key), Units.LUX));
}
}
}
}
}
}

View File

@ -0,0 +1,101 @@
/*
* Copyright (c) 2010-2025 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.dirigera.internal.handler.sensor;
import static org.openhab.binding.dirigera.internal.Constants.*;
import java.util.Iterator;
import java.util.Map;
import java.util.TreeMap;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.json.JSONObject;
import org.openhab.binding.dirigera.internal.interfaces.Model;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.unit.Units;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
/**
* The {@link MotionLightSensorHandler} basic DeviceHandler for all devices
*
* @author Bernd Weymann - Initial contribution
*/
@NonNullByDefault
public class MotionLightSensorHandler extends MotionSensorHandler {
private TreeMap<String, String> relations = new TreeMap<>();
public MotionLightSensorHandler(Thing thing, Map<String, String> mapping) {
super(thing, mapping);
super.setChildHandler(this);
}
@Override
public void initialize() {
super.initialize();
if (super.checkHandler()) {
JSONObject values = gateway().api().readDevice(config.id);
handleUpdate(values);
// assure deviceType is set from main device
if (values.has(PROPERTY_DEVICE_TYPE)) {
deviceType = values.getString(PROPERTY_DEVICE_TYPE);
}
// get all relations and register
String relationId = gateway().model().getRelationId(config.id);
relations = gateway().model().getRelations(relationId);
// register for updates of twin devices
relations.forEach((key, value) -> {
gateway().registerDevice(this, key);
JSONObject relationValues = gateway().api().readDevice(key);
handleUpdate(relationValues);
});
}
}
@Override
public void dispose() {
relations.forEach((key, value) -> {
gateway().unregisterDevice(this, key);
});
super.dispose();
}
@Override
public void handleRemoval() {
relations.forEach((key, value) -> {
gateway().deleteDevice(this, key);
});
super.handleRemoval();
}
@Override
public void handleUpdate(JSONObject update) {
super.handleUpdate(update);
if (update.has(Model.ATTRIBUTES)) {
JSONObject attributes = update.getJSONObject(Model.ATTRIBUTES);
Iterator<String> attributesIterator = attributes.keys();
while (attributesIterator.hasNext()) {
String key = attributesIterator.next();
String targetChannel = property2ChannelMap.get(key);
if (targetChannel != null) {
if (CHANNEL_ILLUMINANCE.equals(targetChannel)) {
updateState(new ChannelUID(thing.getUID(), targetChannel),
QuantityType.valueOf(attributes.getInt(key), Units.LUX));
}
}
}
}
}
}

View File

@ -0,0 +1,284 @@
/*
* Copyright (c) 2010-2025 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.dirigera.internal.handler.sensor;
import static org.openhab.binding.dirigera.internal.Constants.*;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.json.JSONArray;
import org.json.JSONObject;
import org.openhab.binding.dirigera.internal.handler.BaseHandler;
import org.openhab.binding.dirigera.internal.interfaces.Model;
import org.openhab.core.library.types.DateTimeType;
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.library.unit.Units;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.types.Command;
import org.openhab.core.types.UnDefType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link MotionSensorHandler}
*
* @author Bernd Weymann - Initial contribution
*/
@NonNullByDefault
public class MotionSensorHandler extends BaseHandler {
private final Logger logger = LoggerFactory.getLogger(MotionSensorHandler.class);
private final String timeFormat = "HH:mm";
private String startTime = "20:00";
private String endTime = "07:00";
public MotionSensorHandler(Thing thing, Map<String, String> mapping) {
super(thing, mapping);
super.setChildHandler(this);
// links of types which can be established towards this device
linkCandidateTypes = List.of(DEVICE_TYPE_LIGHT, DEVICE_TYPE_OUTLET);
}
@Override
public void initialize() {
super.initialize();
if (super.checkHandler()) {
JSONObject values = gateway().api().readDevice(config.id);
handleUpdate(values);
}
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
super.handleCommand(channelUID, command);
String targetChannel = channelUID.getIdWithoutGroup();
switch (targetChannel) {
case CHANNEL_ACTIVE_DURATION:
int seconds = -1;
if (command instanceof DecimalType decimal) {
seconds = decimal.intValue();
} else if (command instanceof QuantityType<?> quantity) {
QuantityType<?> secondsQunatity = quantity.toUnit(Units.SECOND);
if (secondsQunatity != null) {
seconds = secondsQunatity.intValue();
}
}
if (seconds > 0) {
String updateData = String
.format(gateway().model().getTemplate(Model.TEMPLATE_SENSOR_DURATION_UPDATE), seconds);
sendPatch(new JSONObject(updateData));
}
break;
case CHANNEL_SCHEDULE:
if (command instanceof DecimalType decimal) {
switch (decimal.intValue()) {
case 0:
gateway().api().sendPatch(config.id,
new JSONObject(gateway().model().getTemplate(Model.TEMPLATE_SENSOR_ALWQAYS_ON)));
break;
case 1:
gateway().api().sendPatch(config.id,
new JSONObject(gateway().model().getTemplate(Model.TEMPLATE_SENSOR_FOLLOW_SUN)));
break;
case 2:
String template = gateway().model().getTemplate(Model.TEMPLATE_SENSOR_SCHEDULE_ON);
gateway().api().sendPatch(config.id,
new JSONObject(String.format(template, startTime, endTime)));
break;
}
}
break;
case CHANNEL_SCHEDULE_START:
String startSchedule = gateway().model().getTemplate(Model.TEMPLATE_SENSOR_SCHEDULE_ON);
if (command instanceof StringType string) {
// take string as it is, no consistency check
startTime = string.toFullString();
} else if (command instanceof DateTimeType dateTime) {
startTime = dateTime.format(timeFormat, ZoneId.systemDefault());
}
gateway().api().sendPatch(config.id, new JSONObject(String.format(startSchedule, startTime, endTime)));
break;
case CHANNEL_SCHEDULE_END:
String endSchedule = gateway().model().getTemplate(Model.TEMPLATE_SENSOR_SCHEDULE_ON);
if (command instanceof StringType string) {
endTime = string.toFullString();
// take string as it is, no consistency check
} else if (command instanceof DateTimeType dateTime) {
endTime = dateTime.format(timeFormat, ZoneId.systemDefault());
}
gateway().api().sendPatch(config.id, new JSONObject(String.format(endSchedule, startTime, endTime)));
break;
case CHANNEL_LIGHT_PRESET:
if (command instanceof StringType string) {
JSONArray presetValues = new JSONArray();
// handle the standard presets from IKEA app, custom otherwise without consistency check
switch (string.toFullString()) {
case "Off":
// fine - array stays empty
break;
case "Warm":
presetValues = new JSONArray(
gateway().model().getTemplate(Model.TEMPLATE_LIGHT_PRESET_WARM));
break;
case "Slowdown":
presetValues = new JSONArray(
gateway().model().getTemplate(Model.TEMPLATE_LIGHT_PRESET_SLOWDOWN));
break;
case "Smooth":
presetValues = new JSONArray(
gateway().model().getTemplate(Model.TEMPLATE_LIGHT_PRESET_SMOOTH));
break;
case "Bright":
presetValues = new JSONArray(
gateway().model().getTemplate(Model.TEMPLATE_LIGHT_PRESET_BRIGHT));
break;
default:
presetValues = new JSONArray(string.toFullString());
}
JSONObject preset = new JSONObject();
preset.put("circadianPresets", presetValues);
super.sendAttributes(preset);
}
}
}
@Override
public void handleUpdate(JSONObject update) {
super.handleUpdate(update);
if (update.has(Model.ATTRIBUTES)) {
JSONObject attributes = update.getJSONObject(Model.ATTRIBUTES);
Iterator<String> attributesIterator = attributes.keys();
while (attributesIterator.hasNext()) {
String key = attributesIterator.next();
String targetChannel = property2ChannelMap.get(key);
if (targetChannel != null) {
switch (targetChannel) {
case CHANNEL_MOTION_DETECTION:
updateState(new ChannelUID(thing.getUID(), targetChannel),
OnOffType.from(attributes.getBoolean(key)));
break;
case CHANNEL_ACTIVE_DURATION:
if (attributes.has("sensorConfig")) {
JSONObject sensorConfig = attributes.getJSONObject("sensorConfig");
if (sensorConfig.has("onDuration")) {
int duration = sensorConfig.getInt("onDuration");
updateState(new ChannelUID(thing.getUID(), targetChannel),
QuantityType.valueOf(duration, Units.SECOND));
}
}
break;
}
}
// no direct channel mapping - sensor mapping is deeply nested :(
switch (key) {
case "circadianPresets":
if (attributes.has("circadianPresets")) {
JSONArray lightPresets = attributes.getJSONArray("circadianPresets");
updateState(new ChannelUID(thing.getUID(), CHANNEL_LIGHT_PRESET),
StringType.valueOf(lightPresets.toString()));
}
break;
case "sensorConfig":
if (attributes.has("sensorConfig")) {
JSONObject sensorConfig = attributes.getJSONObject("sensorConfig");
if (sensorConfig.has("scheduleOn")) {
boolean scheduled = sensorConfig.getBoolean("scheduleOn");
if (scheduled) {
// examine schedule
if (sensorConfig.has("schedule")) {
JSONObject schedule = sensorConfig.getJSONObject("schedule");
if (schedule.has("onCondition") && schedule.has("offCondition")) {
JSONObject onCondition = schedule.getJSONObject("onCondition");
JSONObject offCondition = schedule.getJSONObject("offCondition");
if (onCondition.has("time")) {
String onTime = onCondition.getString("time");
String offTime = offCondition.getString("time");
if ("sunset".equals(onTime)) {
// finally it's identified to follow the sun
updateState(new ChannelUID(thing.getUID(), CHANNEL_SCHEDULE),
new DecimalType(1));
Instant sunsetDateTime = gateway().getSunsetDateTime();
if (sunsetDateTime != null) {
updateState(
new ChannelUID(thing.getUID(), CHANNEL_SCHEDULE_START),
new DateTimeType(sunsetDateTime));
} else {
updateState(
new ChannelUID(thing.getUID(), CHANNEL_SCHEDULE_START),
UnDefType.UNDEF);
logger.warn(
"MOTION_SENSOR Location not activated in IKEA App - cannot follow sun");
}
Instant sunriseDateTime = gateway().getSunriseDateTime();
if (sunriseDateTime != null) {
updateState(
new ChannelUID(thing.getUID(), CHANNEL_SCHEDULE_END),
new DateTimeType(sunriseDateTime));
} else {
updateState(
new ChannelUID(thing.getUID(), CHANNEL_SCHEDULE_END),
UnDefType.UNDEF);
logger.warn(
"MOTION_SENSOR Location not activated in IKEA App - cannot follow sun");
}
} else {
// custom times - even worse parsing
String[] onHourMinute = onTime.split(":");
String[] offHourMinute = offTime.split(":");
if (onHourMinute.length == 2 && offHourMinute.length == 2) {
int onHour = Integer.parseInt(onHourMinute[0]);
int onMinute = Integer.parseInt(onHourMinute[1]);
int offHour = Integer.parseInt(offHourMinute[0]);
int offMinute = Integer.parseInt(offHourMinute[1]);
updateState(new ChannelUID(thing.getUID(), CHANNEL_SCHEDULE),
new DecimalType(2));
ZonedDateTime on = ZonedDateTime.now().withHour(onHour)
.withMinute(onMinute);
ZonedDateTime off = ZonedDateTime.now().withHour(offHour)
.withMinute(offMinute);
updateState(
new ChannelUID(thing.getUID(), CHANNEL_SCHEDULE_START),
new DateTimeType(on));
updateState(
new ChannelUID(thing.getUID(), CHANNEL_SCHEDULE_END),
new DateTimeType(off));
}
}
}
}
}
} else {
// always active
updateState(new ChannelUID(thing.getUID(), CHANNEL_SCHEDULE), new DecimalType(0));
updateState(new ChannelUID(thing.getUID(), CHANNEL_SCHEDULE_START),
UnDefType.UNDEF);
updateState(new ChannelUID(thing.getUID(), CHANNEL_SCHEDULE_END), UnDefType.UNDEF);
}
}
}
break;
}
}
}
}
}

View File

@ -0,0 +1,78 @@
/*
* Copyright (c) 2010-2025 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.dirigera.internal.handler.sensor;
import static org.openhab.binding.dirigera.internal.Constants.CHANNEL_LEAK_DETECTION;
import java.util.Arrays;
import java.util.Iterator;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.json.JSONObject;
import org.openhab.binding.dirigera.internal.handler.BaseHandler;
import org.openhab.binding.dirigera.internal.interfaces.Model;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.types.Command;
/**
* The {@link WaterSensorHandler} basic DeviceHandler for all devices
*
* @author Bernd Weymann - Initial contribution
*/
@NonNullByDefault
public class WaterSensorHandler extends BaseHandler {
public WaterSensorHandler(Thing thing, Map<String, String> mapping) {
super(thing, mapping);
super.setChildHandler(this);
// no link support for Scenes
hardLinks = Arrays.asList();
}
@Override
public void initialize() {
super.initialize();
if (super.checkHandler()) {
JSONObject values = gateway().api().readDevice(config.id);
handleUpdate(values);
}
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
super.handleCommand(channelUID, command);
}
@Override
public void handleUpdate(JSONObject update) {
super.handleUpdate(update);
// now device specific
if (update.has(Model.ATTRIBUTES)) {
JSONObject attributes = update.getJSONObject(Model.ATTRIBUTES);
Iterator<String> attributesIterator = attributes.keys();
while (attributesIterator.hasNext()) {
String key = attributesIterator.next();
String targetChannel = property2ChannelMap.get(key);
if (targetChannel != null) {
if (CHANNEL_LEAK_DETECTION.equals(targetChannel)) {
updateState(new ChannelUID(thing.getUID(), targetChannel),
OnOffType.from(attributes.getBoolean(key)));
}
}
}
}
}
}

View File

@ -0,0 +1,234 @@
/*
* Copyright (c) 2010-2025 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.dirigera.internal.handler.speaker;
import static org.openhab.binding.dirigera.internal.Constants.*;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.json.JSONObject;
import org.openhab.binding.dirigera.internal.handler.BaseHandler;
import org.openhab.binding.dirigera.internal.interfaces.Model;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.NextPreviousType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.PercentType;
import org.openhab.core.library.types.PlayPauseType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.openhab.core.types.State;
import org.openhab.core.types.UnDefType;
/**
* The {@link SpeakerHandler} to control speaker devices
*
* @author Bernd Weymann - Initial contribution
*/
@NonNullByDefault
public class SpeakerHandler extends BaseHandler {
public SpeakerHandler(Thing thing, Map<String, String> mapping) {
super(thing, mapping);
super.setChildHandler(this);
// links of types which can be established towards this device
linkCandidateTypes = List.of(DEVICE_TYPE_SOUND_CONTROLLER);
}
@Override
public void initialize() {
super.initialize();
if (super.checkHandler()) {
JSONObject values = gateway().api().readDevice(config.id);
handleUpdate(values);
}
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
String channel = channelUID.getIdWithoutGroup();
if (command instanceof RefreshType) {
super.handleCommand(channelUID, command);
} else {
String targetProperty = channel2PropertyMap.get(channel);
if (targetProperty != null) {
switch (channel) {
case CHANNEL_PLAYER:
if (command instanceof PlayPauseType playPause) {
String playState = (PlayPauseType.PLAY.equals(playPause) ? "playbackPlaying"
: "playbackPaused");
JSONObject attributes = new JSONObject();
attributes.put(targetProperty, playState);
super.sendAttributes(attributes);
} else if (command instanceof NextPreviousType nextPrevious) {
String playState = (NextPreviousType.NEXT.equals(nextPrevious) ? "playbackNext"
: "playbackPrevious");
JSONObject attributes = new JSONObject();
attributes.put(targetProperty, playState);
super.sendAttributes(attributes);
}
break;
case CHANNEL_VOLUME:
if (command instanceof PercentType percent) {
JSONObject attributes = new JSONObject();
attributes.put(targetProperty, percent.intValue());
super.sendAttributes(attributes);
}
break;
case CHANNEL_MUTE:
if (command instanceof OnOffType onOff) {
JSONObject attributes = new JSONObject();
attributes.put(targetProperty, OnOffType.ON.equals(onOff));
super.sendAttributes(attributes);
}
break;
}
} else {
// handle channels not in map due to deeper nesting objects
switch (channel) {
case CHANNEL_SHUFFLE:
if (command instanceof OnOffType onOff) {
JSONObject mode = new JSONObject();
mode.put("shuffle", OnOffType.ON.equals(onOff));
JSONObject attributes = new JSONObject();
attributes.put("playbackModes", mode);
super.sendAttributes(attributes);
}
break;
case CHANNEL_CROSSFADE:
if (command instanceof OnOffType onOff) {
JSONObject mode = new JSONObject();
mode.put("crossfade", OnOffType.ON.equals(onOff));
JSONObject attributes = new JSONObject();
attributes.put("playbackModes", mode);
super.sendAttributes(attributes);
}
break;
case CHANNEL_REPEAT:
if (command instanceof DecimalType decimal) {
int repeatModeInt = decimal.intValue();
String repeatModeStr = "";
switch (repeatModeInt) {
case 0:
repeatModeStr = "off";
break;
case 1:
repeatModeStr = "playItem";
break;
case 2:
repeatModeStr = "playlist";
break;
}
if (!repeatModeStr.isBlank()) {
JSONObject mode = new JSONObject();
mode.put("repeat", repeatModeStr);
JSONObject attributes = new JSONObject();
attributes.put("playbackModes", mode);
super.sendAttributes(attributes);
}
}
break;
}
}
}
}
@Override
public void handleUpdate(JSONObject update) {
super.handleUpdate(update);
if (update.has(Model.ATTRIBUTES)) {
JSONObject attributes = update.getJSONObject(Model.ATTRIBUTES);
Iterator<String> attributesIterator = attributes.keys();
while (attributesIterator.hasNext()) {
String key = attributesIterator.next();
String targetChannel = property2ChannelMap.get(key);
if (targetChannel != null) {
if (CHANNEL_PLAYER.equals(targetChannel)) {
String playerState = attributes.getString(key);
switch (playerState) {
case "playbackPlaying":
updateState(new ChannelUID(thing.getUID(), targetChannel), PlayPauseType.PLAY);
break;
case "playbackIdle":
case "playbackPaused":
updateState(new ChannelUID(thing.getUID(), targetChannel), PlayPauseType.PAUSE);
break;
}
} else if (CHANNEL_VOLUME.equals(targetChannel)) {
updateState(new ChannelUID(thing.getUID(), targetChannel),
new PercentType(attributes.getInt(key)));
} else if (CHANNEL_MUTE.equals(targetChannel)) {
updateState(new ChannelUID(thing.getUID(), targetChannel),
OnOffType.from(attributes.getBoolean(key)));
} else if (CHANNEL_PLAY_MODES.equals(targetChannel)) {
JSONObject playbackModes = attributes.getJSONObject(key);
if (playbackModes.has("crossfade")) {
updateState(new ChannelUID(thing.getUID(), CHANNEL_CROSSFADE),
OnOffType.from(playbackModes.getBoolean("crossfade")));
}
if (playbackModes.has("shuffle")) {
updateState(new ChannelUID(thing.getUID(), CHANNEL_SHUFFLE),
OnOffType.from(playbackModes.getBoolean("shuffle")));
}
if (playbackModes.has("repeat")) {
String repeatMode = playbackModes.getString("repeat");
int playMode = -1;
switch (repeatMode) {
case "off":
playMode = 0;
break;
case "playItem":
playMode = 1;
break;
case "playlist":
playMode = 2;
break;
}
if (playMode != -1) {
updateState(new ChannelUID(thing.getUID(), CHANNEL_REPEAT), new DecimalType(playMode));
}
}
} else if (CHANNEL_TRACK.equals(targetChannel)) {
// track is nested into attributes playItem
State track = UnDefType.UNDEF;
State image = UnDefType.UNDEF;
JSONObject audio = attributes.getJSONObject(key);
if (audio.has("playItem")) {
JSONObject playItem = audio.getJSONObject("playItem");
if (playItem.has("title")) {
track = new StringType(playItem.getString("title"));
}
if (playItem.has("imageURL")) {
String imageURL = playItem.getString("imageURL");
image = gateway().api().getImage(imageURL);
}
} else if (audio.has("playlist")) {
JSONObject playlist = audio.getJSONObject("playlist");
if (playlist.has("title")) {
track = new StringType(playlist.getString("title"));
}
}
updateState(new ChannelUID(thing.getUID(), targetChannel), track);
updateState(new ChannelUID(thing.getUID(), CHANNEL_IMAGE), image);
}
}
}
}
}
}

View File

@ -0,0 +1,60 @@
/*
* Copyright (c) 2010-2025 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.dirigera.internal.interfaces;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.binding.ThingHandler;
/**
* {@link DebugHandler} interface to control debugging via rule actions
*
* @author Bernd Weymann - Initial contribution
*/
@NonNullByDefault
public interface DebugHandler extends ThingHandler {
/**
* Returns the token associated with the DIRIGERA gateway. Regardless on which device this action is called the
* token from gateway (bridge) is returned.
*
* @return token as String
*/
String getToken();
/**
* Returns the JSON representation at this time for a specific device. If action is called on gateway a snapshot
* from all connected devices is returned.
*
* @return device JSON at this time
*/
String getJSON();
/**
* Enables / disables debug for one specific device. If enabled messages are logged on info level regarding
* - commands send via openHAB
* - state updates of openHAB
* - API requests with payload towards gateway
* - push notifications from gateway
* - API responses from gateway
*
* @param debug boolean flag enabling or disabling debug messages
*/
void setDebug(boolean debug, boolean all);
/**
* Returns the device ID of the device this handler is associated with.
*
* @return device ID as String
*/
String getDeviceId();
}

View File

@ -0,0 +1,113 @@
/*
* Copyright (c) 2010-2025 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.dirigera.internal.interfaces;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.json.JSONObject;
import org.openhab.core.types.State;
/**
* {@link DirigeraAPI} high level interface to communicate with the gateway. These are comfort functions fitting to the
* needs of the handlers. Each function is synchronized so no parallel calls will be established towards gateway.
* Rationale:
* Several times seen that gateway goes into a "quite mode" during monkey testing. It's still accepting commands but no
* more updates were received.
*
* @author Bernd Weymann - Initial contribution
*/
@NonNullByDefault
public interface DirigeraAPI {
/** JSON key for error flag, value shall be boolean */
static final String HTTP_ERROR_FLAG = "http-error-flag";
/** JSON key for error flag, value shall be int */
static final String HTTP_ERROR_STATUS = "http-error-status";
/** JSON key for error message, value shall be String */
static final String HTTP_ERROR_MESSAGE = "http-error-message";
/**
* Read complete home model.
*
* @return JSONObject with data. In case of error the JSONObject is filled with error data
*/
JSONObject readHome();
/**
* Read all data for one specific deviceId.
*
* @param deviceId to query
* @return JSONObject with data. In case of error the JSONObject is filled with error data
*/
JSONObject readDevice(String deviceId);
/**
* Read all data for one specific scene.
*
* @param sceneId to query
* @return JSONObject with data. In case of error the JSONObject is filled with error data
*/
JSONObject readScene(String sceneId);
/**
* Read all data for one specific scene.
*
* @param sceneId to query
* @param trigger to send
* @return JSONObject with data. In case of error the JSONObject is filled with error data
*/
void triggerScene(String sceneId, String trigger);
/**
* Send attributes to a device
*
* @param deviceId to update
* @param attributes to send
* @return Integer of http response status
*/
int sendAttributes(String deviceId, JSONObject attributes);
/**
* Send patch with other data than attributes to a device
*
* @param deviceId to update
* @param data to send
* @return Integer of http response status
*/
int sendPatch(String deviceId, JSONObject data);
/**
* Creating a scene from scene template for a click pattern of a controller
*
* @param uuid of the scene to be created
* @param clickPattern which shall trigger the scene
* @param controllerId which delivering the clickPattern
* @return String uuid of the created scene
*/
String createScene(String uuid, String clickPattern, String controllerId);
/**
* Delete scene of given uuid
*
* @param uuid of the scene to be deleted
*/
void deleteScene(String uuid);
/**
* Get image from an url.
*
* @return RawType in case of successful call, UndefType.UNDEF in case of error
*/
State getImage(String imageURL);
}

View File

@ -0,0 +1,193 @@
/*
* Copyright (c) 2010-2025 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.dirigera.internal.interfaces;
import java.time.Instant;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.dirigera.internal.DirigeraCommandProvider;
import org.openhab.binding.dirigera.internal.discovery.DirigeraDiscoveryService;
import org.openhab.binding.dirigera.internal.exception.ApiException;
import org.openhab.binding.dirigera.internal.exception.ModelException;
import org.openhab.binding.dirigera.internal.handler.BaseHandler;
import org.openhab.core.thing.Thing;
import org.osgi.framework.BundleContext;
/**
* The {@link Gateway} Gateway interface to access data from other instances.
*
* @author Bernd Weymann - Initial contribution
*/
@NonNullByDefault
public interface Gateway {
/**
* Get the thing attached to this gateway.
*
* @return Thing
*/
Thing getThing();
/**
* Get IP address from gateway for API calls and WebSocket connections.
*
* @return ip address as String
*/
String getIpAddress();
/**
* Get token associated to this gateway for API calls and WebSocket connections.
*
* @return token as String
*/
String getToken();
/**
* Get CommandProvider associated to this binding. For links and link candidates the command options are filled with
* the right link options.
*
* @return DirigeraCommandProvider as DynamicCommandDescriptionProvider
*/
DirigeraCommandProvider getCommandProvider();
/**
* Returns the configuration setting if discovery is enabled.
*
* @return boolean discovery flag
*/
boolean discoveryEnabled();
/**
* Register a handler with the given deviceId reflecting a device or scene. Shall be called during
* initialization.
*
* This function is handled asynchronous.
*
* @param deviceHandler handler of this binding
* @param deviceId connected device id
*/
void registerDevice(BaseHandler deviceHandler, String deviceId);
/**
* Unregister a handler associated with the given deviceId reflecting a device or scene. Shall be called
* during dispose.
*
* This function is handled asynchronous.
*
* @param deviceHandler handler of this binding
* @param deviceId connected device id
*/
void unregisterDevice(BaseHandler deviceHandler, String deviceId);
/**
* Deletes an openHAB handler associated with the given deviceId reflecting a device or scene. Shall be called
* during handleRemoval.
*
* This function is handled asynchronous.
*
* @param deviceHandler handler of this binding
* @param deviceId connected device id
*/
void deleteDevice(BaseHandler deviceHandler, String deviceId);
/**
* Deletes a device or scene detected by the model. A device can be deleted without openHAB interaction in IKEA Home
* smart app and openHAB needs to be informed about this removal to update ThingStatus accordingly.
*
* @param deviceId device id to be removed
*/
void deleteDevice(String deviceId);
/**
* Check if device id is known in the gateway namely if a handler is created or not.
*
* @param deviceId connected device id
*/
boolean isKnownDevice(String deviceId);
/**
* Update websocket connected statues.
*
* @param boolean connected
* @param reason as String
*/
void websocketConnected(boolean connected, String reason);
/**
* Update from websocket regarding changed data.
*
* This function is handled asynchronous.
*
* @param String content of update
*/
void websocketUpdate(String update);
/**
* Update links for all devices. Devices which are storing the links (hard link) are responsible to detect changes.
* If change is detected the linked device will be updated with a soft link.
*
* This function is handled asynchronous.
*
* @param String content of update
*/
void updateLinks();
/**
* Next sunrise ZonedDateTime. Value is presented if gateway allows access to GPS position. Handler needs to take
* care regarding null values.
*
* @return next sunrise as ZonedDateTime
*/
@Nullable
Instant getSunriseDateTime();
/**
* Next sunset ZonedDateTime. Value is presented if gateway allows access to GPS position. Handler needs to take
* care regarding null values.
*
* @return next sunrise as ZonedDateTime
*/
@Nullable
Instant getSunsetDateTime();
/**
* Comfort access towards API which is only present after initialization.
*
* @throws ApiMissingException
* @return DirigeraAPI
*/
DirigeraAPI api() throws ApiException;
/**
* Comfort access towards Model which is only present after initialization.
*
* @throws ModelMissingException
* @return Model
*/
Model model() throws ModelException;
/**
* Comfort access towards DirigeraDiscoveryManager.
*
* @return DirigeraDiscoveryManager
*/
DirigeraDiscoveryService discovery();
/**
* Comfort access towards DirigeraDiscoveryManager.
*
* @return DirigeraDiscoveryManager
*/
BundleContext getBundleContext();
}

View File

@ -0,0 +1,181 @@
/*
* Copyright (c) 2010-2025 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.dirigera.internal.interfaces;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.json.JSONObject;
import org.openhab.core.thing.ThingTypeUID;
/**
* The {@link Model} is representing the structural data of the gateway. Concrete values e.g. temperature of devices
* shall not be accessed.
*
* @author Bernd Weymann - Initial contribution
*/
@NonNullByDefault
public interface Model {
static final String REACHABLE = "isReachable";
static final String ATTRIBUTES = "attributes";
static final String CAPABILITIES = "capabilities";
static final String PROPERTY_CAN_RECEIVE = "canReceive";
static final String PROPERTY_CAN_SEND = "canSend";
static final String SCENES = "scenes";
static final String CUSTOM_NAME = "customName";
static final String DEVICE_MODEL = "model";
static final String DEVICE_TYPE = "deviceType";
static final String PROPERTY_RELATION_ID = "relationId";
static final String COLOR_TEMPERATURE_CAPABILITY = "colorTemperature";
static final String TEMPLATE_LIGHT_PRESET_BRIGHT = "/json/light-presets/bright.json";
static final String TEMPLATE_LIGHT_PRESET_SLOWDOWN = "/json/light-presets/slowdown.json";
static final String TEMPLATE_LIGHT_PRESET_SMOOTH = "/json/light-presets/smooth.json";
static final String TEMPLATE_LIGHT_PRESET_WARM = "/json/light-presets/warm.json";
static final String TEMPLATE_SENSOR_ALWQAYS_ON = "/json/sensor-config/always-on.json";
static final String TEMPLATE_SENSOR_DURATION_UPDATE = "/json/sensor-config/duration-update.json";
static final String TEMPLATE_SENSOR_FOLLOW_SUN = "/json/sensor-config/follow-sun.json";
static final String TEMPLATE_SENSOR_SCHEDULE_ON = "/json/sensor-config/schedule-on.json";
static final String TEMPLATE_CLICK_SCENE = "/json/scenes/click-scene.json";
static final String TEMPLATE_COORDINATES = "/json/gateway/coordinates.json";
static final String TEMPLATE_NULL_COORDINATES = "/json/gateway/null-coordinates.json";
/**
* Get structure model as JSON String.
*
* @see json channel
* @return JSON String
*/
String getModelString();
/**
* Model update will be performed with API request. Relative expensive operation depending on number of connected
* devices. Call triggers
* - startup
* - add / remove device to DIRIGERA gateway, not openHAB
* - custom name changes for Discovery updates
*/
int update();
/**
* Starts a new detection without model update. If handlers are removed they shall appear in discovery again.
*/
void detection();
/**
* Get all id's for a specific type. Used to identify link candidates for a specific device.
* - LightController needs lights and plugs and vice versa
* - BlindController needs blinds and vice versa
* - SoundController needs speakers and vice versa
*
* @param types as list of types to query
* @return list of matching device id's
*/
List<String> getDevicesForTypes(List<String> types);
/**
* Returns a list of all device id's.
*
* @return list of all connected devices
*/
List<String> getAllDeviceIds();
/**
* Returns a list with resolved relation id's. There are complex device registering more than one id with different
* type. This binding combines them in one handler.
* - MotionLightHandler
* - DoubleShortcutControllerHandler
*
* @return list of device id's without related devices
*/
List<String> getResolvedDeviceList();
/**
* Get all stored information for one device or scene.
*
* @param id to query
* @param type device or scene
* @return data as JSON
*/
JSONObject getAllFor(String id, String type);
/**
* Gets all relations marked into relationId property
* Rationale:
* VALLHORN Motion Sensor registers 2 devices
* - Motion Sensor
* - Light Sensor
*
* Shortcut Controller with 2 buttons registers 2 controllers
* They shall not be splitted in 2 different things so one Thing shall receive updates for both id's
*
* Use TreeMap to sort device id's so suffix _1 comes before _2
*
* @param relationId
* @return List of id's with same serial number
*/
TreeMap<String, String> getRelations(String relationId);
/**
* Get relationId for a given device id
*
* @param id to check
* @return same id if no relations are found or relationId
*/
String getRelationId(String id);
/**
* Identify device which is present in model with openHAB ThingTypeUID.
*
* @param id to identify
* @return ThingTypeUID
*/
ThingTypeUID identifyDeviceFromModel(String id);
/**
* Check if given id is present in devices or scenes.
*
* @param id to check
* @return true if id is found
*/
boolean has(String id);
/**
* Get the custom name configured in IKEA Smart home app.
*
* @param id to query
* @return name as String
*/
String getCustonNameFor(String id);
/**
* Properties Map for Discovery
*
* @param id to query
* @return Map with attributes for Thing properties
*/
Map<String, Object> getPropertiesFor(String id);
/**
* Read a resource file from this bundle. Some presets and commands sent to API shall not be implemented
* in code if they are just needing minor String replacements.
* Root path in project is src/main/resources. Line breaks and white spaces will
*
* @return
*/
String getTemplate(String name);
}

View File

@ -0,0 +1,34 @@
/*
* Copyright (c) 2010-2025 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.dirigera.internal.interfaces;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.library.types.OnOffType;
/**
* {@link PowerListener} for notifications of device power events
*
* @author Bernd Weymann - Initial contribution
*/
@NonNullByDefault
public interface PowerListener {
/**
* Informs if power state of device has changed.
*
* @param power new power state
* @param requested flag showing if new power state was requested by OH user command or from outside (e.g wall
* mounted switch)
*/
void powerChanged(OnOffType power, boolean requested);
}

View File

@ -0,0 +1,169 @@
/*
* Copyright (c) 2010-2025 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.dirigera.internal.model;
import java.util.Map.Entry;
import java.util.TreeMap;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.library.types.HSBType;
import org.openhab.core.library.types.PercentType;
import org.openhab.core.util.ColorUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link ColorModel} converts colors according to DIRIGERA values. openHAB ColorUtil conversion uses XY
* transformations which visually are not matching e.g. for kelvin2HSB values.
*
* @author Bernd Weymann - Initial contribution
*/
@NonNullByDefault
public class ColorModel {
private static final Logger LOGGER = LoggerFactory.getLogger(ColorModel.class);
private static final TreeMap<Integer, Integer> MAPPING_RGB_TEMPERETATURE = new TreeMap<>();
private static final TreeMap<Integer, Integer> MAPPING_TEMPERETATURE_RGB = new TreeMap<>();
private static final int MAX_HUE = 360;
private static final int MAX_SAT = 100;
/**
* Simulate color-temperature if color light doesn't have the "canReceive" "colorTemperature" capability
* https://www.npmjs.com/package/color-temperature?activeTab=code
*
* @param kelvin
* @return color temperature as HSBType
*/
private static int[] kelvin2RGB(long kelvin) {
double temperature = kelvin / 100.0;
double red;
double green;
double blue;
/* Calculate red */
if (temperature <= 66.0) {
red = 255;
} else {
red = temperature - 60.0;
red = 329.698727446 * Math.pow(red, -0.1332047592);
if (red < 0) {
red = 0;
}
if (red > 255) {
red = 255;
}
}
/* Calculate green */
if (temperature <= 66.0) {
green = temperature;
green = 99.4708025861 * Math.log(green) - 161.1195681661;
if (green < 0) {
green = 0;
}
if (green > 255) {
green = 255;
}
} else {
green = temperature - 60.0;
green = 288.1221695283 * Math.pow(green, -0.0755148492);
if (green < 0) {
green = 0;
}
if (green > 255) {
green = 255;
}
}
/* Calculate blue */
if (temperature >= 66.0) {
blue = 255;
} else {
if (temperature <= 19.0) {
blue = 0;
} else {
blue = temperature - 10;
blue = 138.5177312231 * Math.log(blue) - 305.0447927307;
if (blue < 0) {
blue = 0;
}
if (blue > 255) {
blue = 255;
}
}
}
return new int[] { (int) Math.round(red), (int) Math.round(green), (int) Math.round(blue) };
}
private static void init() {
if (MAPPING_RGB_TEMPERETATURE.isEmpty()) {
for (int i = 1000; i < 10001; i = i + 10) {
int rgbEncoding = encodeRGBValue(kelvin2RGB(i));
MAPPING_RGB_TEMPERETATURE.put(rgbEncoding, i);
MAPPING_TEMPERETATURE_RGB.put(i, rgbEncoding);
}
}
}
private static int encodeRGBValue(int[] rgb) {
return rgb[0] * 1000000 + rgb[1] * 1000 + rgb[2];
}
private static int[] decodeRGBValue(int encoded) {
int part = encoded;
int red = part / 1000000;
part -= red * 1000000;
int green = part / 1000;
part -= green * 1000;
int blue = part;
return new int[] { red, green, blue };
}
public static HSBType kelvin2Hsb(long kelvin) {
init();
Entry<Integer, Integer> entry = MAPPING_TEMPERETATURE_RGB.ceilingEntry((int) kelvin);
if (entry == null) {
entry = MAPPING_TEMPERETATURE_RGB.floorEntry((int) kelvin);
if (entry == null) {
// this path cannot be entered if tables isn't empty which is prevent by init call
LOGGER.warn("DIRIGERA COLOR_MODEL no rgb mapping found for {}", kelvin);
return new HSBType();
}
}
int encoded = entry.getValue();
int[] rgb = decodeRGBValue(encoded);
return ColorUtil.rgbToHsb(rgb);
}
public static long hsb2Kelvin(HSBType hsb) {
init();
HSBType compare = new HSBType(hsb.getHue(), hsb.getSaturation(), PercentType.HUNDRED);
int rgb[] = ColorUtil.hsbToRgb(compare);
int key = encodeRGBValue(rgb);
Entry<Integer, Integer> entry = MAPPING_RGB_TEMPERETATURE.ceilingEntry(key);
if (entry == null) {
entry = MAPPING_RGB_TEMPERETATURE.floorEntry(key);
if (entry == null) {
// this path cannot be entered if tables isn't empty which is prevent by init call
LOGGER.warn("DIRIGERA COLOR_MODEL no kelvin mapping found for {}", compare);
return -1;
}
}
return entry.getValue();
}
public static boolean closeTo(HSBType refHSB, HSBType compareHSB, double percent) {
double hueDistance = Math.abs(refHSB.getHue().doubleValue() - compareHSB.getHue().doubleValue());
double saturationDistance = Math
.abs(refHSB.getSaturation().doubleValue() - compareHSB.getSaturation().doubleValue());
return ((hueDistance < (MAX_HUE * percent) || hueDistance > (MAX_HUE - (MAX_HUE * percent)))
&& saturationDistance < (MAX_SAT * percent));
}
}

View File

@ -0,0 +1,563 @@
/*
* Copyright (c) 2010-2025 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.dirigera.internal.model;
import static org.openhab.binding.dirigera.internal.Constants.*;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Scanner;
import java.util.TreeMap;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.json.JSONArray;
import org.json.JSONObject;
import org.openhab.binding.dirigera.internal.interfaces.DirigeraAPI;
import org.openhab.binding.dirigera.internal.interfaces.Gateway;
import org.openhab.binding.dirigera.internal.interfaces.Model;
import org.openhab.core.config.discovery.DiscoveryResult;
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.ThingUID;
import org.osgi.framework.Bundle;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link DirigeraModel} is representing the structural data of the devices connected to gateway. Concrete values of
* devices shall not be accessed.
*
* @author Bernd Weymann - Initial contribution
*/
@NonNullByDefault
public class DirigeraModel implements Model {
private final Logger logger = LoggerFactory.getLogger(DirigeraModel.class);
private Map<String, DiscoveryResult> resultMap = new HashMap<>();
private Map<String, String> templates = new HashMap<>();
private List<String> devices = new ArrayList<>();
private JSONObject model = new JSONObject();
private Gateway gateway;
public DirigeraModel(Gateway gateway) {
this.gateway = gateway;
}
@Override
public synchronized String getModelString() {
return model.toString();
}
@Override
public synchronized int update() {
Instant startTime = Instant.now();
JSONObject home = gateway.api().readHome();
// call finished with error code ...
if (home.has(DirigeraAPI.HTTP_ERROR_FLAG)) {
int status = home.getInt(DirigeraAPI.HTTP_ERROR_STATUS);
logger.warn("DIRIGERA MODEL received model with error code {} - don't take it", status);
return status;
} else if (home.isEmpty()) {
// ... call finished with unchecked exception ...
return 500;
} else {
// ... call finished with success
model = home;
detection();
}
logger.trace("DIRIGERA MODEL full update {} ms", Duration.between(startTime, Instant.now()).toMillis());
return 200;
}
@Override
public synchronized void detection() {
if (gateway.discoveryEnabled()) {
List<String> previousDevices = new ArrayList<>();
previousDevices.addAll(devices);
// first get devices
List<String> foundDevices = new ArrayList<>();
foundDevices.addAll(getResolvedDeviceList());
foundDevices.addAll(getAllSceneIds());
devices.clear();
devices.addAll(foundDevices);
previousDevices.forEach(deviceId -> {
boolean known = gateway.isKnownDevice(deviceId);
boolean removed = !foundDevices.contains(deviceId);
if (removed) {
removedDeviceScene(deviceId);
} else {
if (!known) {
addedDeviceScene(deviceId);
} // don't update known devices
}
});
foundDevices.removeAll(previousDevices);
foundDevices.forEach(deviceId -> {
boolean known = gateway.isKnownDevice(deviceId);
if (!known) {
addedDeviceScene(deviceId);
}
});
}
}
/**
* Returns list with resolved relations
*
* @return
*/
@Override
public synchronized List<String> getResolvedDeviceList() {
List<String> deviceList = new ArrayList<>();
if (!model.isNull(PROPERTY_DEVICES)) {
JSONArray devices = model.getJSONArray(PROPERTY_DEVICES);
Iterator<Object> entries = devices.iterator();
while (entries.hasNext()) {
JSONObject entry = (JSONObject) entries.next();
String deviceId = entry.getString(PROPERTY_DEVICE_ID);
String relationId = getRelationId(deviceId);
if (!deviceId.equals(relationId)) {
TreeMap<String, String> relationMap = getRelations(relationId);
// store for complex devices store result with first found id
relationId = relationMap.firstKey();
}
if (!deviceList.contains(relationId)) {
deviceList.add(relationId);
}
}
}
return deviceList;
}
/**
* Returns list with all device id's
*
* @return
*/
@Override
public synchronized List<String> getAllDeviceIds() {
List<String> deviceList = new ArrayList<>();
if (!model.isNull(PROPERTY_DEVICES)) {
JSONArray devices = model.getJSONArray(PROPERTY_DEVICES);
Iterator<Object> entries = devices.iterator();
while (entries.hasNext()) {
JSONObject entry = (JSONObject) entries.next();
deviceList.add(entry.getString(PROPERTY_DEVICE_ID));
}
}
return deviceList;
}
private List<String> getAllSceneIds() {
List<String> sceneList = new ArrayList<>();
if (!model.isNull(SCENES)) {
JSONArray scenes = model.getJSONArray(SCENES);
Iterator<Object> sceneIterator = scenes.iterator();
while (sceneIterator.hasNext()) {
JSONObject entry = (JSONObject) sceneIterator.next();
if (entry.has(PROPERTY_TYPE)) {
if ("userScene".equals(entry.getString(PROPERTY_TYPE))) {
if (entry.has(PROPERTY_DEVICE_ID)) {
String id = entry.getString(PROPERTY_DEVICE_ID);
sceneList.add(id);
}
}
}
}
}
return sceneList;
}
private void addedDeviceScene(String id) {
DiscoveryResult result = identifiy(id);
if (result != null) {
gateway.discovery().deviceDiscovered(result);
resultMap.put(id, result);
}
}
private void removedDeviceScene(String id) {
DiscoveryResult deliveredResult = resultMap.remove(id);
if (deliveredResult != null) {
gateway.discovery().deviceRemoved(deliveredResult);
}
// inform gateway to remove device and update handler accordingly
gateway.deleteDevice(id);
}
@Override
public synchronized List<String> getDevicesForTypes(List<String> types) {
List<String> candidates = new ArrayList<>();
types.forEach(type -> {
JSONArray addons = getIdsForType(type);
addons.forEach(entry -> {
candidates.add(entry.toString());
});
});
return candidates;
}
private JSONArray getIdsForType(String type) {
JSONArray returnArray = new JSONArray();
if (!model.isNull(PROPERTY_DEVICES)) {
JSONArray devices = model.getJSONArray(PROPERTY_DEVICES);
Iterator<Object> entries = devices.iterator();
while (entries.hasNext()) {
JSONObject entry = (JSONObject) entries.next();
if (!entry.isNull(PROPERTY_DEVICE_TYPE) && !entry.isNull(PROPERTY_DEVICE_ID)) {
if (type.equals(entry.get(PROPERTY_DEVICE_TYPE))) {
returnArray.put(entry.get(PROPERTY_DEVICE_ID));
}
}
}
}
return returnArray;
}
private boolean hasAttribute(String id, String attribute) {
JSONObject deviceObject = getAllFor(id, PROPERTY_DEVICES);
if (deviceObject.has(ATTRIBUTES)) {
JSONObject attributes = deviceObject.getJSONObject(ATTRIBUTES);
return attributes.has(attribute);
}
return false;
}
@Override
public synchronized JSONObject getAllFor(String id, String type) {
JSONObject returnObject = new JSONObject();
if (model.has(type)) {
JSONArray devices = model.getJSONArray(type);
Iterator<Object> entries = devices.iterator();
while (entries.hasNext()) {
JSONObject entry = (JSONObject) entries.next();
if (id.equals(entry.get(PROPERTY_DEVICE_ID))) {
return entry;
}
}
}
return returnObject;
}
@Override
public synchronized String getCustonNameFor(String id) {
JSONObject deviceObject = getAllFor(id, PROPERTY_DEVICES);
if (deviceObject.has(ATTRIBUTES)) {
JSONObject attributes = deviceObject.getJSONObject(ATTRIBUTES);
if (attributes.has(CUSTOM_NAME)) {
String customName = attributes.getString(CUSTOM_NAME);
if (!customName.isBlank()) {
return customName;
}
}
if (attributes.has(DEVICE_MODEL)) {
String deviceModel = attributes.getString(DEVICE_MODEL);
if (!deviceModel.isBlank()) {
return deviceModel;
}
}
if (deviceObject.has(DEVICE_TYPE)) {
return deviceObject.getString(DEVICE_TYPE);
}
// 3 fallback options
}
// not found yet - check scenes
JSONObject sceneObject = getAllFor(id, PROPERTY_SCENES);
if (sceneObject.has("info")) {
JSONObject info = sceneObject.getJSONObject("info");
if (info.has("name")) {
String name = info.getString("name");
if (!name.isBlank()) {
return name;
}
}
}
return id;
}
@Override
public synchronized Map<String, Object> getPropertiesFor(String id) {
final Map<String, Object> properties = new HashMap<>();
JSONObject deviceObject = getAllFor(id, PROPERTY_DEVICES);
// get manufacturer, model and version data
if (deviceObject.has(ATTRIBUTES)) {
JSONObject attributes = deviceObject.getJSONObject(ATTRIBUTES);
THING_PROPERTIES.forEach(property -> {
if (attributes.has(property)) {
properties.put(property, attributes.get(property));
}
});
}
// put id in as representation property
properties.put(PROPERTY_DEVICE_ID, id);
// add capabilities
if (deviceObject.has(CAPABILITIES)) {
JSONObject capabilities = deviceObject.getJSONObject(CAPABILITIES);
if (capabilities.has(PROPERTY_CAN_RECEIVE)) {
properties.put(PROPERTY_CAN_RECEIVE, capabilities.getJSONArray(PROPERTY_CAN_RECEIVE));
}
if (capabilities.has(PROPERTY_CAN_SEND)) {
properties.put(PROPERTY_CAN_SEND, capabilities.getJSONArray(PROPERTY_CAN_SEND));
}
}
return properties;
}
@Override
public synchronized TreeMap<String, String> getRelations(String relationId) {
final TreeMap<String, String> relationsMap = new TreeMap<>();
List<String> allDevices = getAllDeviceIds();
allDevices.forEach(deviceId -> {
JSONObject data = getAllFor(deviceId, PROPERTY_DEVICES);
if (data.has(Model.PROPERTY_RELATION_ID)) {
String relation = data.getString(Model.PROPERTY_RELATION_ID);
if (relationId.equals(relation)) {
String relationDeviceId = data.getString(PROPERTY_DEVICE_ID);
String deviceType = data.getString(PROPERTY_DEVICE_TYPE);
if (relationDeviceId != null && deviceType != null) {
relationsMap.put(relationDeviceId, deviceType);
}
}
}
});
return relationsMap;
}
private @Nullable DiscoveryResult identifiy(String id) {
ThingTypeUID ttuid = identifyDeviceFromModel(id);
// don't report gateway, unknown devices and light sensors connected to motion sensors
if (!THING_TYPE_GATEWAY.equals(ttuid) && !THING_TYPE_UNKNNOWN.equals(ttuid)
&& !THING_TYPE_LIGHT_SENSOR.equals(ttuid) && !THING_TYPE_IGNORE.equals(ttuid)) {
// check if it's a simple or complex device
String relationId = getRelationId(id);
String firstDeviceId = id;
if (!id.equals(relationId)) {
// complex device
TreeMap<String, String> relationMap = getRelations(relationId);
// take name from first ordered entry
firstDeviceId = relationMap.firstKey();
}
// take name and properties from first found id
String customName = getCustonNameFor(firstDeviceId);
Map<String, Object> propertiesMap = getPropertiesFor(firstDeviceId);
return DiscoveryResultBuilder.create(new ThingUID(ttuid, gateway.getThing().getUID(), firstDeviceId))
.withBridge(gateway.getThing().getUID()).withProperties(propertiesMap)
.withRepresentationProperty(PROPERTY_DEVICE_ID).withLabel(customName).build();
}
return null;
}
/**
* Identify device which is present in model
*
* @param id
* @return
*/
@Override
public synchronized ThingTypeUID identifyDeviceFromModel(String id) {
JSONObject entry = getAllFor(id, PROPERTY_DEVICES);
if (entry.isEmpty()) {
entry = getAllFor(id, PROPERTY_SCENES);
}
if (entry.isEmpty()) {
return THING_TYPE_NOT_FOUND;
} else {
return identifyDeviceFromJSON(id, entry);
}
}
private ThingTypeUID identifyDeviceFromJSON(String id, JSONObject data) {
String typeDeviceType = "";
if (data.has(Model.PROPERTY_RELATION_ID)) {
return identifiyComplexDevice(data.getString(Model.PROPERTY_RELATION_ID));
} else if (data.has(PROPERTY_DEVICE_TYPE)) {
String deviceType = data.getString(PROPERTY_DEVICE_TYPE);
typeDeviceType = deviceType;
switch (deviceType) {
case DEVICE_TYPE_GATEWAY:
return THING_TYPE_GATEWAY;
case DEVICE_TYPE_LIGHT:
if (data.has(CAPABILITIES)) {
JSONObject capabilities = data.getJSONObject(CAPABILITIES);
List<String> capabilityList = new ArrayList<>();
if (capabilities.has(PROPERTY_CAN_RECEIVE)) {
JSONArray receiveProperties = capabilities.getJSONArray(PROPERTY_CAN_RECEIVE);
receiveProperties.forEach(capability -> {
capabilityList.add(capability.toString());
});
}
if (capabilityList.contains("colorHue")) {
return THING_TYPE_COLOR_LIGHT;
} else if (capabilityList.contains("colorTemperature")) {
return THING_TYPE_TEMPERATURE_LIGHT;
} else if (capabilityList.contains("lightLevel")) {
return THING_TYPE_DIMMABLE_LIGHT;
} else if (capabilityList.contains("isOn")) {
return THING_TYPE_SWITCH_LIGHT;
} else {
logger.warn("DIRIGERA MODEL cannot identify light {}", data);
}
} else {
logger.warn("DIRIGERA MODEL cannot identify light {}", data);
}
break;
case DEVICE_TYPE_MOTION_SENSOR:
return THING_TYPE_MOTION_SENSOR;
case DEVICE_TYPE_LIGHT_SENSOR:
return THING_TYPE_LIGHT_SENSOR;
case DEVICE_TYPE_CONTACT_SENSOR:
return THING_TYPE_CONTACT_SENSOR;
case DEVICE_TYPE_OUTLET:
if (hasAttribute(id, "currentActivePower")) {
return THING_TYPE_SMART_PLUG;
} else if (hasAttribute(id, "childLock")) {
return THING_TYPE_POWER_PLUG;
} else {
return THING_TYPE_SIMPLE_PLUG;
}
case DEVICE_TYPE_SPEAKER:
return THING_TYPE_SPEAKER;
case DEVICE_TYPE_REPEATER:
return THING_TYPE_REPEATER;
case DEVICE_TYPE_LIGHT_CONTROLLER:
return THING_TYPE_LIGHT_CONTROLLER;
case DEVICE_TYPE_ENVIRONMENT_SENSOR:
return THING_TYPE_AIR_QUALITY;
case DEVICE_TYPE_WATER_SENSOR:
return THING_TYPE_WATER_SENSOR;
case DEVICE_TYPE_AIR_PURIFIER:
return THING_TYPE_AIR_PURIFIER;
case DEVICE_TYPE_BLINDS:
return THING_TYPE_BLIND;
case DEVICE_TYPE_BLIND_CONTROLLER:
return THING_TYPE_BLIND_CONTROLLER;
case DEVICE_TYPE_SOUND_CONTROLLER:
return THING_TYPE_SOUND_CONTROLLER;
case DEVICE_TYPE_SHORTCUT_CONTROLLER:
return THING_TYPE_SINGLE_SHORTCUT_CONTROLLER;
}
} else {
// device type is empty, check for scene
if (!data.isNull(PROPERTY_TYPE)) {
String type = data.getString(PROPERTY_TYPE);
typeDeviceType = type + "/" + typeDeviceType; // just for logging
switch (type) {
case TYPE_USER_SCENE:
return THING_TYPE_SCENE;
case TYPE_CUSTOM_SCENE:
return THING_TYPE_IGNORE;
}
}
}
logger.warn("DIRIGERA MODEL Unsupported device {} with data {} {}", typeDeviceType, data, id);
return THING_TYPE_UNKNNOWN;
}
private ThingTypeUID identifiyComplexDevice(String relationId) {
Map<String, String> relationsMap = getRelations(relationId);
if (relationsMap.size() == 2 && relationsMap.containsValue("lightSensor")
&& relationsMap.containsValue("motionSensor")) {
return THING_TYPE_MOTION_LIGHT_SENSOR;
} else if (relationsMap.size() == 2 && relationsMap.containsValue("shortcutController")) {
for (Iterator<String> iterator = relationsMap.keySet().iterator(); iterator.hasNext();) {
if (!"shortcutController".equals(relationsMap.get(iterator.next()))) {
return THING_TYPE_UNKNNOWN;
}
}
return THING_TYPE_DOUBLE_SHORTCUT_CONTROLLER;
} else if (relationsMap.size() == 1 && relationsMap.containsValue("gatewy")) {
return THING_TYPE_GATEWAY;
} else {
return THING_TYPE_UNKNNOWN;
}
}
/**
* Get relationId for a given device id
*
* @param id to check
* @return same id if no relations are found or relationId
*/
@Override
public synchronized String getRelationId(String id) {
JSONObject dataObject = getAllFor(id, PROPERTY_DEVICES);
if (dataObject.has(PROPERTY_RELATION_ID)) {
return dataObject.getString(PROPERTY_RELATION_ID);
}
return id;
}
/**
* Check if given id is present in devices or scenes
*
* @param id to check
* @return true if id is found
*/
@Override
public synchronized boolean has(String id) {
return getAllDeviceIds().contains(id) || getAllSceneIds().contains(id);
}
@Override
public String getTemplate(String name) {
String template = templates.get(name);
if (template == null) {
template = getResourceFile(name);
if (!template.isBlank()) {
templates.put(name, template);
} else {
logger.warn("DIRIGERA MODEL empty template for {}", name);
template = "{}";
}
}
return template;
}
private String getResourceFile(String fileName) {
try {
Bundle myself = gateway.getBundleContext().getBundle();
// do this check for unit tests to avoid NullPointerException
if (myself != null) {
URL url = myself.getResource(fileName);
InputStream input = url.openStream();
// https://www.baeldung.com/java-scanner-usedelimiter
try (Scanner scanner = new Scanner(input).useDelimiter("\\A")) {
String result = scanner.hasNext() ? scanner.next() : "";
String resultReplaceAll = result.replaceAll("[\\n\\r\\s]", "");
scanner.close();
return resultReplaceAll;
}
} else {
// only unit testing
return Files.readString(Paths.get("src/main/resources" + fileName));
}
} catch (IOException e) {
logger.warn("DIRIGERA MODEL no template found for {}", fileName);
}
return "";
}
}

View File

@ -0,0 +1,324 @@
/*
* Copyright (c) 2010-2025 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.dirigera.internal.network;
import static org.openhab.binding.dirigera.internal.Constants.*;
import java.nio.charset.StandardCharsets;
import java.util.Set;
import java.util.TreeSet;
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.client.HttpClient;
import org.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.client.util.StringContentProvider;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.websocket.api.annotations.WebSocket;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.openhab.binding.dirigera.internal.interfaces.DirigeraAPI;
import org.openhab.binding.dirigera.internal.interfaces.Gateway;
import org.openhab.binding.dirigera.internal.interfaces.Model;
import org.openhab.core.library.types.RawType;
import org.openhab.core.types.State;
import org.openhab.core.types.UnDefType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* {@link DirigeraAPIImpl} provides easy access towards REST API
*
* @author Bernd Weymann - Initial contribution
*/
@WebSocket
@NonNullByDefault
public class DirigeraAPIImpl implements DirigeraAPI {
private final Logger logger = LoggerFactory.getLogger(DirigeraAPIImpl.class);
private static final String GENREAL_LOCK = "lock";
private Set<String> activeCallers = new TreeSet<>();
private HttpClient httpClient;
private Gateway gateway;
public DirigeraAPIImpl(HttpClient httpClient, Gateway gateway) {
this.httpClient = httpClient;
this.gateway = gateway;
}
private Request addAuthorizationHeader(Request sourceRequest) {
if (!gateway.getToken().isBlank()) {
return sourceRequest.header(HttpHeader.AUTHORIZATION, "Bearer " + gateway.getToken());
} else {
logger.warn("DIRIGERA API Cannot operate with token {}", gateway.getToken());
return sourceRequest;
}
}
@Override
public JSONObject readHome() {
String url = String.format(HOME_URL, gateway.getIpAddress());
JSONObject statusObject = new JSONObject();
startCalling(GENREAL_LOCK);
try {
Request homeRequest = httpClient.newRequest(url);
ContentResponse response = addAuthorizationHeader(homeRequest).timeout(10, TimeUnit.SECONDS).send();
int responseStatus = response.getStatus();
if (responseStatus == 200) {
statusObject = new JSONObject(response.getContentAsString());
} else {
statusObject = getErrorJson(responseStatus, response.getReason());
}
return statusObject;
} catch (InterruptedException | TimeoutException | ExecutionException | JSONException e) {
logger.warn("DIRIGERA API Exception calling {}", url);
statusObject = getErrorJson(500, e.getMessage());
return statusObject;
} finally {
endCalling(GENREAL_LOCK);
}
}
@Override
public JSONObject readDevice(String deviceId) {
String url = String.format(DEVICE_URL, gateway.getIpAddress(), deviceId);
JSONObject statusObject = new JSONObject();
startCalling(deviceId);
try {
Request homeRequest = httpClient.newRequest(url);
ContentResponse response = addAuthorizationHeader(homeRequest).timeout(10, TimeUnit.SECONDS).send();
int responseStatus = response.getStatus();
if (responseStatus == 200) {
statusObject = new JSONObject(response.getContentAsString());
} else {
statusObject = getErrorJson(responseStatus, response.getReason());
}
return statusObject;
} catch (InterruptedException | TimeoutException | ExecutionException | JSONException e) {
logger.warn("DIRIGERA API Exception calling {}", url);
statusObject = getErrorJson(500, e.getMessage());
return statusObject;
} finally {
endCalling(deviceId);
}
}
@Override
public void triggerScene(String sceneId, String trigger) {
String url = String.format(SCENE_URL, gateway.getIpAddress(), sceneId) + "/" + trigger;
startCalling(sceneId);
try {
Request homeRequest = httpClient.POST(url);
ContentResponse response = addAuthorizationHeader(homeRequest).timeout(10, TimeUnit.SECONDS).send();
int responseStatus = response.getStatus();
if (responseStatus != 200 && responseStatus != 202) {
logger.warn("DIRIGERA API Scene trigger failed with {}", responseStatus);
}
} catch (InterruptedException | TimeoutException | ExecutionException e) {
logger.warn("DIRIGERA API Exception calling {}", url);
} finally {
endCalling(sceneId);
}
}
@Override
public int sendAttributes(String id, JSONObject attributes) {
JSONObject data = new JSONObject();
data.put(Model.ATTRIBUTES, attributes);
return sendPatch(id, data);
}
@Override
public int sendPatch(String id, JSONObject data) {
String url = String.format(DEVICE_URL, gateway.getIpAddress(), id);
// pack attributes into data json and then into an array
JSONArray dataArray = new JSONArray();
dataArray.put(data);
StringContentProvider stringProvider = new StringContentProvider("application/json", dataArray.toString(),
StandardCharsets.UTF_8);
Request deviceRequest = httpClient.newRequest(url).method("PATCH")
.header(HttpHeader.CONTENT_TYPE, "application/json").content(stringProvider);
int responseStatus = 500;
startCalling(id);
try {
ContentResponse response = addAuthorizationHeader(deviceRequest).timeout(10, TimeUnit.SECONDS).send();
responseStatus = response.getStatus();
if (responseStatus == 200 || responseStatus == 202) {
logger.debug("DIRIGERA API send finished {} with {} {}", url, dataArray, responseStatus);
} else {
logger.warn("DIRIGERA API send failed {} with {} {}", url, dataArray, responseStatus);
}
return responseStatus;
} catch (InterruptedException | TimeoutException | ExecutionException e) {
logger.warn("DIRIGERA API send failed {} failed {} {}", url, dataArray, e.getMessage());
return responseStatus;
} finally {
endCalling(id);
}
}
@Override
public State getImage(String imageURL) {
State image = UnDefType.UNDEF;
startCalling(GENREAL_LOCK);
try {
ContentResponse response = httpClient.GET(imageURL);
if (response.getStatus() == 200) {
String mimeType = response.getMediaType();
if (mimeType == null) {
mimeType = RawType.DEFAULT_MIME_TYPE;
}
image = new RawType(response.getContent(), mimeType);
} else {
logger.warn("DIRIGERA API call to {} failed {}", imageURL, response.getStatus());
}
return image;
} catch (InterruptedException | ExecutionException | TimeoutException e) {
logger.warn("DIRIGERA API call to {} failed {}", imageURL, e.getMessage());
return image;
} finally {
endCalling(GENREAL_LOCK);
}
}
@Override
public JSONObject readScene(String sceneId) {
String url = String.format(SCENE_URL, gateway.getIpAddress(), sceneId);
JSONObject statusObject = new JSONObject();
Request homeRequest = httpClient.newRequest(url);
startCalling(GENREAL_LOCK);
try {
ContentResponse response = addAuthorizationHeader(homeRequest).timeout(10, TimeUnit.SECONDS).send();
int responseStatus = response.getStatus();
if (responseStatus == 200) {
statusObject = new JSONObject(response.getContentAsString());
} else {
statusObject = getErrorJson(responseStatus, response.getReason());
}
return statusObject;
} catch (InterruptedException | TimeoutException | ExecutionException | JSONException e) {
logger.warn("DIRIGERA API Exception calling {}", url);
statusObject = getErrorJson(-1, e.getMessage());
return statusObject;
} finally {
endCalling(GENREAL_LOCK);
}
}
@Override
public String createScene(String uuid, String clickPattern, String controllerId) {
String url = String.format(SCENES_URL, gateway.getIpAddress());
String sceneTemplate = gateway.model().getTemplate(Model.TEMPLATE_CLICK_SCENE);
String payload = String.format(sceneTemplate, uuid, "openHAB Shortcut Proxy", clickPattern, "0", controllerId);
StringContentProvider stringProvider = new StringContentProvider("application/json", payload,
StandardCharsets.UTF_8);
Request sceneCreateRequest = httpClient.newRequest(url).method("POST")
.header(HttpHeader.CONTENT_TYPE, "application/json").content(stringProvider);
int responseStatus = 500;
String responseUUID = "";
int retryCounter = 3;
startCalling(GENREAL_LOCK);
try {
while (retryCounter > 0 && !uuid.equals(responseUUID)) {
try {
ContentResponse response = addAuthorizationHeader(sceneCreateRequest).timeout(10, TimeUnit.SECONDS)
.send();
responseStatus = response.getStatus();
if (responseStatus == 200 || responseStatus == 202) {
logger.debug("DIRIGERA API send {} to {} delivered", payload, url);
String responseString = response.getContentAsString();
JSONObject responseJSON = new JSONObject(responseString);
responseUUID = responseJSON.getString(PROPERTY_DEVICE_ID);
break;
} else {
logger.warn("DIRIGERA API send {} to {} failed with status {}", payload, url,
response.getStatus());
}
} catch (InterruptedException | TimeoutException | ExecutionException | JSONException e) {
logger.warn("DIRIGERA API call to {} failed {}", url, e.getMessage());
}
logger.debug("DIRIGERA API createScene failed {} retries remaining", retryCounter);
retryCounter--;
}
return responseUUID;
} finally {
endCalling(GENREAL_LOCK);
}
}
@Override
public void deleteScene(String uuid) {
String url = String.format(SCENES_URL, gateway.getIpAddress()) + "/" + uuid;
Request sceneDeleteRequest = httpClient.newRequest(url).method("DELETE");
int responseStatus = 500;
int retryCounter = 3;
startCalling(GENREAL_LOCK);
try {
while (retryCounter > 0 && responseStatus != 200 && responseStatus != 202) {
try {
ContentResponse response = addAuthorizationHeader(sceneDeleteRequest).timeout(10, TimeUnit.SECONDS)
.send();
responseStatus = response.getStatus();
if (responseStatus == 200 || responseStatus == 202) {
logger.debug("DIRIGERA API delete {} performed", url);
break;
} else {
logger.warn("DIRIGERA API send {} failed with status {}", url, response.getStatus());
}
} catch (InterruptedException | TimeoutException | ExecutionException e) {
logger.warn("DIRIGERA API call to {} failed {}", url, e.getMessage());
}
logger.debug("DIRIGERA API deleteScene failed with status {}, {} retries remaining", responseStatus,
retryCounter);
retryCounter--;
}
} finally {
endCalling(GENREAL_LOCK);
}
}
public JSONObject getErrorJson(int status, @Nullable String message) {
String error = String.format(
"{\"http-error-flag\":true,\"http-error-status\":%s,\"http-error-message\":\"%s\"}", status, message);
return new JSONObject(error);
}
private void startCalling(String uuid) {
synchronized (this) {
while (activeCallers.contains(uuid)) {
try {
this.wait();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
// abort execution
return;
}
}
activeCallers.add(uuid);
}
}
private void endCalling(String uuid) {
synchronized (this) {
activeCallers.remove(uuid);
this.notifyAll();
}
}
}

View File

@ -0,0 +1,250 @@
/*
* Copyright (c) 2010-2025 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.dirigera.internal.network;
import static org.openhab.binding.dirigera.internal.Constants.WS_URL;
import java.io.IOException;
import java.net.URI;
import java.nio.ByteBuffer;
import java.time.Duration;
import java.time.Instant;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketError;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketFrame;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage;
import org.eclipse.jetty.websocket.api.annotations.WebSocket;
import org.eclipse.jetty.websocket.api.extensions.Frame;
import org.eclipse.jetty.websocket.client.ClientUpgradeRequest;
import org.eclipse.jetty.websocket.client.WebSocketClient;
import org.json.JSONObject;
import org.openhab.binding.dirigera.internal.interfaces.Gateway;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* {@link Websocket} listens to device changes
*
* @author Bernd Weymann - Initial contribution
*/
@WebSocket
@NonNullByDefault
public class Websocket {
private final Logger logger = LoggerFactory.getLogger(Websocket.class);
private final Map<String, Instant> pingPongMap = new HashMap<>();
private static final String STARTS = "starts";
private static final String STOPS = "stops";
private static final String DISCONNECTS = "disconnetcs";
private static final String ERRORS = "errors";
private static final String PINGS = "pings";
private static final String PING_LATENCY = "pingLatency";
private static final String PING_LAST = "lastPing";
private static final String MESSAGES = "messages";
public static final String MODEL_UPDATES = "modelUpdates";
public static final String MODEL_UPDATE_TIME = "modelUpdateDuration";
public static final String MODEL_UPDATE_LAST = "lastModelUpdate";
private Optional<WebSocketClient> websocketClient = Optional.empty();
private Optional<Session> session = Optional.empty();
private JSONObject statistics = new JSONObject();
private HttpClient httpClient;
private Gateway gateway;
private boolean disposed = false;
public Websocket(Gateway gateway, HttpClient httpClient) {
this.gateway = gateway;
this.httpClient = httpClient;
}
public void initialize() {
disposed = false;
}
public void start() {
if ("unit-test".equals(gateway.getToken())) {
// handle unit tests online
gateway.websocketConnected(true, "unit test");
return;
}
if (disposed) {
logger.debug("DIRIGERA WS start rejected, disposed {}", disposed);
return;
}
increase(STARTS);
internalStop(); // don't count this internal stopping
try {
pingPongMap.clear();
WebSocketClient client = new WebSocketClient(httpClient);
client.setMaxIdleTimeout(0);
ClientUpgradeRequest request = new ClientUpgradeRequest();
request.setHeader("Authorization", "Bearer " + gateway.getToken());
String websocketURL = String.format(WS_URL, gateway.getIpAddress());
logger.trace("DIRIGERA WS start {}", websocketURL);
websocketClient = Optional.of(client);
client.start();
client.connect(this, new URI(websocketURL), request);
} catch (Exception t) {
// catch Exceptions of start stop and declare communication error
logger.warn("DIRIGERA WS handling exception: {}", t.getMessage());
}
}
public boolean isRunning() {
return websocketClient.isPresent() && session.isPresent() && session.get().isOpen();
}
public void stop() {
increase(STOPS);
internalStop();
}
private void internalStop() {
session.ifPresent(session -> {
session.close();
});
websocketClient.ifPresent(client -> {
try {
client.stop();
client.destroy();
} catch (Exception e) {
logger.warn("DIRIGERA WS exception stopping running client");
}
});
websocketClient = Optional.empty();
this.session = Optional.empty();
}
public void dispose() {
internalStop();
disposed = true;
}
public void ping() {
session.ifPresentOrElse((session) -> {
try {
// build ping message
String pingId = UUID.randomUUID().toString();
pingPongMap.put(pingId, Instant.now());
session.getRemote().sendPing(ByteBuffer.wrap(pingId.getBytes()));
increase(PINGS);
} catch (IOException e) {
logger.warn("DIRIGERA WS ping failed with exception {}", e.getMessage());
}
}, () -> {
logger.debug("DIRIGERA WS ping found no session - restart websocket");
});
}
/**
* endpoints
*/
@OnWebSocketMessage
public void onTextMessage(String message) {
increase(MESSAGES);
gateway.websocketUpdate(message);
}
@OnWebSocketFrame
public void onFrame(Frame frame) {
if (Frame.Type.PONG.equals(frame.getType())) {
ByteBuffer buffer = frame.getPayload();
byte[] bytes = new byte[frame.getPayloadLength()];
for (int i = 0; i < frame.getPayloadLength(); i++) {
bytes[i] = buffer.get(i);
}
String paylodString = new String(bytes);
Instant sent = pingPongMap.remove(paylodString);
if (sent != null) {
long durationMS = Duration.between(sent, Instant.now()).toMillis();
statistics.put(PING_LATENCY, durationMS);
statistics.put(PING_LAST, Instant.now());
} else {
logger.debug("DIRIGERA WS receiced pong without ping {}", paylodString);
}
} else if (Frame.Type.PING.equals(frame.getType())) {
session.ifPresentOrElse((session) -> {
logger.trace("DIRIGERA onPing ");
ByteBuffer buffer = frame.getPayload();
try {
session.getRemote().sendPong(buffer);
} catch (IOException e) {
logger.warn("DIRIGERA WS onPing answer exception {}", e.getMessage());
}
}, () -> {
logger.debug("DIRIGERA WS onPing answer cannot be initiated");
});
}
}
@OnWebSocketConnect
public void onConnect(Session session) {
logger.debug("DIRIGERA WS onConnect");
this.session = Optional.of(session);
session.setIdleTimeout(-1);
gateway.websocketConnected(true, "connected");
}
@OnWebSocketClose
public void onDisconnect(Session session, int statusCode, String reason) {
logger.debug("DIRIGERA WS onDisconnect Status {} Reason {}", statusCode, reason);
this.session = Optional.empty();
increase(DISCONNECTS);
gateway.websocketConnected(false, reason);
}
@OnWebSocketError
public void onError(Throwable t) {
String message = t.getMessage();
logger.warn("DIRIGERA WS onError {}", message);
this.session = Optional.empty();
if (message == null) {
message = "unknown";
}
increase(ERRORS);
gateway.websocketConnected(false, message);
}
/**
* Helper functions
*/
public JSONObject getStatistics() {
return statistics;
}
public void increase(String key) {
if (statistics.has(key)) {
int counter = statistics.getInt(key);
statistics.put(key, ++counter);
} else {
statistics.put(key, 1);
}
}
public Map<String, Instant> getPingPongMap() {
return pingPongMap;
}
}

View File

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<addon:addon id="dirigera" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:addon="https://openhab.org/schemas/addon/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/addon/v1.0.0 https://openhab.org/schemas/addon-1.0.0.xsd">
<type>binding</type>
<name>DIRIGERA Binding</name>
<description>IKEA Smarthome binding for DIRIGERA Gateway</description>
<connection>local</connection>
<discovery-methods>
<discovery-method>
<service-type>mdns</service-type>
<discovery-parameters>
<discovery-parameter>
<name>mdnsServiceType</name>
<value>_ihsp._tcp.local.</value>
</discovery-parameter>
</discovery-parameters>
</discovery-method>
</discovery-methods>
</addon:addon>

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<config-description:config-descriptions
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0"
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:dirigera:base-device">
<parameter name="id" type="text">
<label>Device Id</label>
<description>Unique id of this device</description>
</parameter>
</config-description>
</config-description:config-descriptions>

View File

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<config-description:config-descriptions
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0"
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:dirigera:color-light">
<parameter name="id" type="text">
<label>Device Id</label>
<description>Unique id of this device</description>
</parameter>
<parameter name="fadeTime" type="integer" unit="ms">
<label>Fade Time</label>
<description>Required time for fade sequnce to color or brightness</description>
<default>750</default>
</parameter>
<parameter name="fadeSequence" type="integer">
<label>Fade Sequence</label>
<description>Define sequence if several light parameters are changed at once</description>
<options>
<option value="0">First brightness, then color</option>
<option value="1">First color, then brightness</option>
</options>
<default>0</default>
</parameter>
</config-description>
</config-description:config-descriptions>

View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<config-description:config-descriptions
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0"
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:dirigera:gateway">
<parameter name="ipAddress" type="text" required="true">
<label>IP Address</label>
<description>Gateway IP Address</description>
</parameter>
<parameter name="id" type="text">
<label>Device Id</label>
<description>Unique id of this gateway</description>
</parameter>
<parameter name="discovery" type="boolean">
<label>Discovery</label>
<description>Configure if paired devices shall be detected by discovery</description>
<default>true</default>
</parameter>
</config-description>
</config-description:config-descriptions>

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<config-description:config-descriptions
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0"
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:dirigera:light-device">
<parameter name="id" type="text">
<label>Device Id</label>
<description>Unique id of this device</description>
</parameter>
<parameter name="fadeTime" type="integer" unit="ms">
<label>Fade Time</label>
<description>Required time for fade sequnce to color or brightness</description>
<default>750</default>
</parameter>
</config-description>
</config-description:config-descriptions>

View File

@ -0,0 +1,388 @@
# add-on
addon.dirigera.name = DIRIGERA Binding
addon.dirigera.description = IKEA Smarthome binding for DIRIGERA Gateway
# thing types
thing-type.dirigera.air-purifier.label = Air Purifier
thing-type.dirigera.air-purifier.description = Air cleaning device with particle filter
thing-type.dirigera.air-purifier.channel.fan-mode.label = Fan Mode
thing-type.dirigera.air-purifier.channel.fan-mode.description = Fan on, off, speed or automatic behavior
thing-type.dirigera.air-purifier.channel.fan-runtime.label = Fan Runtime
thing-type.dirigera.air-purifier.channel.fan-runtime.description = Fan runtime in minutes
thing-type.dirigera.air-purifier.channel.fan-speed.label = Fan Speed
thing-type.dirigera.air-purifier.channel.fan-speed.description = Manual regulation of fan speed
thing-type.dirigera.air-purifier.channel.filter-alarm.label = Filter Alarm
thing-type.dirigera.air-purifier.channel.filter-alarm.description = Filter alarm signal
thing-type.dirigera.air-purifier.channel.filter-elapsed.label = Filter Elapsed
thing-type.dirigera.air-purifier.channel.filter-elapsed.description = Filter elapsed time in minutes
thing-type.dirigera.air-purifier.channel.filter-lifetime.label = Filter Lifetime
thing-type.dirigera.air-purifier.channel.filter-lifetime.description = Filter lifetime in minutes
thing-type.dirigera.air-purifier.channel.filter-remain.label = Filter Remain
thing-type.dirigera.air-purifier.channel.filter-remain.description = Remaining filter time in minutes
thing-type.dirigera.air-purifier.channel.particulate-matter.label = Particulate Matter
thing-type.dirigera.air-purifier.channel.particulate-matter.description = Category 2.5 particulate matter
thing-type.dirigera.air-quality.label = Air Quality
thing-type.dirigera.air-quality.description = Air measure for temperature, humidity and particles
thing-type.dirigera.air-quality.channel.humidity.label = Humidity
thing-type.dirigera.air-quality.channel.humidity.description = Atmospheric humidity in percent
thing-type.dirigera.air-quality.channel.particulate-matter.label = Particulate Matter
thing-type.dirigera.air-quality.channel.particulate-matter.description = Category 2.5 particulate matter
thing-type.dirigera.air-quality.channel.temperature.label = Temperature
thing-type.dirigera.air-quality.channel.temperature.description = Current indoor temperature
thing-type.dirigera.air-quality.channel.voc-index.label = VOC Index
thing-type.dirigera.air-quality.channel.voc-index.description = Relative VOC intensity compared to recent history
thing-type.dirigera.blind-controller.label = Blinds Controller
thing-type.dirigera.blind-controller.description = Controller to open and close blinds
thing-type.dirigera.blind-controller.channel.battery-level.label = Battery Charge Level
thing-type.dirigera.blind-controller.channel.battery-level.description = Battery charge level in percent
thing-type.dirigera.blind.label = Blind
thing-type.dirigera.blind.description = Window or door blind
thing-type.dirigera.blind.channel.battery-level.label = Battery Charge Level
thing-type.dirigera.blind.channel.battery-level.description = Battery charge level in percent
thing-type.dirigera.color-light.label = Color Light
thing-type.dirigera.color-light.description = Light with color support
thing-type.dirigera.color-light.channel.brightness.label = Brightness
thing-type.dirigera.color-light.channel.brightness.description = Brightness of light in percent
thing-type.dirigera.color-light.channel.color.label = Color
thing-type.dirigera.color-light.channel.color.description = Color of light with hue, saturation and brightness
thing-type.dirigera.color-light.channel.color-temperature.label = Color Temperature
thing-type.dirigera.color-light.channel.color-temperature.description = Color temperature from cold (0 %) to warm (100 %)
thing-type.dirigera.color-light.channel.power.label = Light Powered
thing-type.dirigera.color-light.channel.power.description = Power state of light
thing-type.dirigera.contact-sensor.label = Contact Sensor
thing-type.dirigera.contact-sensor.description = Sensor tracking if windows or doors are open
thing-type.dirigera.contact-sensor.channel.battery-level.label = Battery Charge Level
thing-type.dirigera.contact-sensor.channel.battery-level.description = Battery charge level in percent
thing-type.dirigera.contact-sensor.channel.contact.label = Contact State
thing-type.dirigera.contact-sensor.channel.contact.description = State if door or window is open or closed
thing-type.dirigera.dimmable-light.label = Dimmable Light
thing-type.dirigera.dimmable-light.description = Light with brightness support
thing-type.dirigera.dimmable-light.channel.brightness.label = Brightness
thing-type.dirigera.dimmable-light.channel.brightness.description = Brightness of light in percent
thing-type.dirigera.dimmable-light.channel.power.label = Light Powered
thing-type.dirigera.dimmable-light.channel.power.description = Power state of light
thing-type.dirigera.double-shortcut.label = Two Button Shortcut
thing-type.dirigera.double-shortcut.description = Shortcut controller with two buttons
thing-type.dirigera.double-shortcut.channel.battery-level.label = Battery Charge Level
thing-type.dirigera.double-shortcut.channel.battery-level.description = Battery charge level in percent
thing-type.dirigera.double-shortcut.channel.button1.label = Button 1 Trigger
thing-type.dirigera.double-shortcut.channel.button1.description = Trigger of first button
thing-type.dirigera.double-shortcut.channel.button2.label = Button 2 Trigger
thing-type.dirigera.double-shortcut.channel.button2.description = Trigger of second button
thing-type.dirigera.gateway.label = DIRIGERA Gateway
thing-type.dirigera.gateway.description = IKEA Gateway for smart products
thing-type.dirigera.gateway.channel.location.label = Home Location
thing-type.dirigera.gateway.channel.location.description = Location in latitude, longitude coordinates
thing-type.dirigera.gateway.channel.ota-progress.label = OTA Progress
thing-type.dirigera.gateway.channel.ota-progress.description = Over-the-air update progress
thing-type.dirigera.gateway.channel.ota-state.label = OTA State
thing-type.dirigera.gateway.channel.ota-state.description = Over-the-air current state
thing-type.dirigera.gateway.channel.ota-status.label = OTA Status
thing-type.dirigera.gateway.channel.ota-status.description = Over-the-air overall status
thing-type.dirigera.gateway.channel.pairing.label = Pairing
thing-type.dirigera.gateway.channel.pairing.description = Sets DIRIGERA hub into pairing mode
thing-type.dirigera.gateway.channel.statistics.label = Gateway Statistics
thing-type.dirigera.gateway.channel.statistics.description = Several statistics about gateway activities
thing-type.dirigera.gateway.channel.sunrise.label = Sunrise
thing-type.dirigera.gateway.channel.sunrise.description = Date and time of next sunrise
thing-type.dirigera.gateway.channel.sunset.label = Sunset
thing-type.dirigera.gateway.channel.sunset.description = Date and time of next sunset
thing-type.dirigera.light-controller.label = Light Controller
thing-type.dirigera.light-controller.description = Controller to handle light attributes
thing-type.dirigera.light-controller.channel.battery-level.label = Battery Charge Level
thing-type.dirigera.light-controller.channel.battery-level.description = Battery charge level in percent
thing-type.dirigera.light-controller.channel.light-preset.label = Light Preset
thing-type.dirigera.light-controller.channel.light-preset.description = Light presets for different times of the day
thing-type.dirigera.light-sensor.label = Light Sensor
thing-type.dirigera.light-sensor.description = Sensor measuring illuminance in your room
thing-type.dirigera.motion-light-sensor.label = Motion Light Sensor
thing-type.dirigera.motion-light-sensor.description = Sensor detecting motion events and measures light level
thing-type.dirigera.motion-light-sensor.channel.active-duration.label = Active Duration
thing-type.dirigera.motion-light-sensor.channel.active-duration.description = Keep connected devices active for this duration
thing-type.dirigera.motion-light-sensor.channel.battery-level.label = Battery Charge Level
thing-type.dirigera.motion-light-sensor.channel.battery-level.description = Battery charge level in percent
thing-type.dirigera.motion-light-sensor.channel.illuminance.label = Illuminance
thing-type.dirigera.motion-light-sensor.channel.illuminance.description = Illuminance in Lux
thing-type.dirigera.motion-light-sensor.channel.light-preset.label = Light Preset
thing-type.dirigera.motion-light-sensor.channel.light-preset.description = Light presets for different times of the day
thing-type.dirigera.motion-light-sensor.channel.motion.label = Motion Detected
thing-type.dirigera.motion-light-sensor.channel.motion.description = Motion detected by the device
thing-type.dirigera.motion-light-sensor.channel.schedule.label = Activity Schedule
thing-type.dirigera.motion-light-sensor.channel.schedule.description = Schedule when the sensor shall be active
thing-type.dirigera.motion-light-sensor.channel.schedule-end.label = Activity Schedule End
thing-type.dirigera.motion-light-sensor.channel.schedule-end.description = End time of sensor activity
thing-type.dirigera.motion-light-sensor.channel.schedule-start.label = Activity Schedule Start
thing-type.dirigera.motion-light-sensor.channel.schedule-start.description = Start time of sensor activity
thing-type.dirigera.motion-sensor.label = Motion Sensor
thing-type.dirigera.motion-sensor.description = Sensor detecting motion events
thing-type.dirigera.motion-sensor.channel.active-duration.label = Active Duration
thing-type.dirigera.motion-sensor.channel.active-duration.description = Keep connected devices active for this duration
thing-type.dirigera.motion-sensor.channel.battery-level.label = Battery Charge Level
thing-type.dirigera.motion-sensor.channel.battery-level.description = Battery charge level in percent
thing-type.dirigera.motion-sensor.channel.light-preset.label = Light Preset
thing-type.dirigera.motion-sensor.channel.light-preset.description = Light presets for different times of the day
thing-type.dirigera.motion-sensor.channel.motion.label = Detection Flag
thing-type.dirigera.motion-sensor.channel.motion.description = Flag if detection happened
thing-type.dirigera.motion-sensor.channel.schedule.label = Activity Schedule
thing-type.dirigera.motion-sensor.channel.schedule.description = Schedule when the sensor shall be active
thing-type.dirigera.motion-sensor.channel.schedule-end.label = Activity Schedule End
thing-type.dirigera.motion-sensor.channel.schedule-end.description = End time of sensor activity
thing-type.dirigera.motion-sensor.channel.schedule-start.label = Activity Schedule Start
thing-type.dirigera.motion-sensor.channel.schedule-start.description = Start time of sensor activity
thing-type.dirigera.power-plug.label = Power Plug
thing-type.dirigera.power-plug.description = Power plug with control of power state, startup behavior, hardware on/off button and status light
thing-type.dirigera.power-plug.channel.power.label = Plug Powered
thing-type.dirigera.power-plug.channel.power.description = Power state of plug
thing-type.dirigera.repeater.label = Repeater
thing-type.dirigera.repeater.description = Repeater to strengthen signal
thing-type.dirigera.scene.label = Scene
thing-type.dirigera.scene.description = Scene from IKEA home smart App which can be triggered
thing-type.dirigera.scene.channel.last-trigger.label = Last Trigger
thing-type.dirigera.scene.channel.last-trigger.description = Date and time when last trigger occurred
thing-type.dirigera.scene.channel.trigger.label = Scene Trigger
thing-type.dirigera.scene.channel.trigger.description = Perform / undo scene execution
thing-type.dirigera.simple-plug.label = Simple Plug
thing-type.dirigera.simple-plug.description = Simple plug with control of power state and startup behavior
thing-type.dirigera.simple-plug.channel.power.label = Plug Powered
thing-type.dirigera.simple-plug.channel.power.description = Power state of plug
thing-type.dirigera.single-shortcut.label = Single Button Shortcut
thing-type.dirigera.single-shortcut.description = Shortcut controller with one button
thing-type.dirigera.single-shortcut.channel.battery-level.label = Battery Charge Level
thing-type.dirigera.single-shortcut.channel.battery-level.description = Battery charge level in percent
thing-type.dirigera.single-shortcut.channel.button1.label = Button 1 Trigger
thing-type.dirigera.single-shortcut.channel.button1.description = Trigger of first button
thing-type.dirigera.smart-plug.label = Smart Power Plug
thing-type.dirigera.smart-plug.description = Power plug with electricity measurements
thing-type.dirigera.smart-plug.channel.electric-current.label = Plug Current
thing-type.dirigera.smart-plug.channel.electric-current.description = Electric current measured by plug
thing-type.dirigera.smart-plug.channel.electric-power.label = Electric Power
thing-type.dirigera.smart-plug.channel.electric-power.description = Electric power delivered by plug
thing-type.dirigera.smart-plug.channel.electric-voltage.label = Plug Voltage
thing-type.dirigera.smart-plug.channel.electric-voltage.description = Electric potential of plug
thing-type.dirigera.smart-plug.channel.energy-reset.label = Energy since Reset
thing-type.dirigera.smart-plug.channel.energy-reset.description = Energy consumption since last reset
thing-type.dirigera.smart-plug.channel.energy-total.label = Total Energy
thing-type.dirigera.smart-plug.channel.energy-total.description = Total energy consumption
thing-type.dirigera.smart-plug.channel.power.label = Plug Powered
thing-type.dirigera.smart-plug.channel.power.description = Power state of plug
thing-type.dirigera.smart-plug.channel.reset-date.label = Reset Date Time
thing-type.dirigera.smart-plug.channel.reset-date.description = Date and time of last reset
thing-type.dirigera.sound-controller.label = Sound Controller
thing-type.dirigera.sound-controller.description = Controller for speakers
thing-type.dirigera.sound-controller.channel.battery-level.label = Battery Charge Level
thing-type.dirigera.sound-controller.channel.battery-level.description = Battery charge level in percent
thing-type.dirigera.speaker.label = Speaker
thing-type.dirigera.speaker.description = Speaker with player activities
thing-type.dirigera.speaker.channel.crossfade.label = Cross Fade
thing-type.dirigera.speaker.channel.crossfade.description = Cross fading between tracks
thing-type.dirigera.speaker.channel.image.label = Image
thing-type.dirigera.speaker.channel.image.description = Current playing track image
thing-type.dirigera.speaker.channel.media-control.label = Media Control
thing-type.dirigera.speaker.channel.media-control.description = Media control play, pause, next, previous
thing-type.dirigera.speaker.channel.media-title.label = Media Title
thing-type.dirigera.speaker.channel.media-title.description = Title of a played media file
thing-type.dirigera.speaker.channel.mute.label = Mute Control
thing-type.dirigera.speaker.channel.mute.description = Mute current audio without stop playing
thing-type.dirigera.speaker.channel.repeat.label = Repeat
thing-type.dirigera.speaker.channel.repeat.description = Repeat Mode
thing-type.dirigera.speaker.channel.shuffle.label = Shuffle
thing-type.dirigera.speaker.channel.shuffle.description = Control shuffle mode
thing-type.dirigera.speaker.channel.volume.label = Volume Control
thing-type.dirigera.speaker.channel.volume.description = Control volume in percent
thing-type.dirigera.switch-light.label = Switch Light
thing-type.dirigera.switch-light.description = Light with switch ON, OFF capability
thing-type.dirigera.switch-light.channel.power.label = Light Powered
thing-type.dirigera.switch-light.channel.power.description = Power state of light
thing-type.dirigera.temperature-light.label = Temperature Light
thing-type.dirigera.temperature-light.description = Light with color temperature support
thing-type.dirigera.temperature-light.channel.brightness.label = Light Brightness
thing-type.dirigera.temperature-light.channel.brightness.description = Brightness of light in percent
thing-type.dirigera.temperature-light.channel.color-temperature.label = Color Temperature
thing-type.dirigera.temperature-light.channel.color-temperature.description = Color temperature from cold (0 %) to warm (100 %)
thing-type.dirigera.temperature-light.channel.power.label = Light Powered
thing-type.dirigera.temperature-light.channel.power.description = Power state of light
thing-type.dirigera.water-sensor.label = Water Sensor
thing-type.dirigera.water-sensor.description = Sensor to detect water leaks
thing-type.dirigera.water-sensor.channel.battery-level.label = Battery Charge Level
thing-type.dirigera.water-sensor.channel.battery-level.description = Battery charge level in percent
thing-type.dirigera.water-sensor.channel.leak.label = Leak Detection
thing-type.dirigera.water-sensor.channel.leak.description = Water leak detection
# thing types config
thing-type.config.dirigera.base-device.id.label = Device Id
thing-type.config.dirigera.base-device.id.description = Unique id of this device
thing-type.config.dirigera.color-light.fadeSequence.label = Fade Sequence
thing-type.config.dirigera.color-light.fadeSequence.description = Define sequence if several light parameters are changed at once
thing-type.config.dirigera.color-light.fadeSequence.option.0 = First brightness, then color
thing-type.config.dirigera.color-light.fadeSequence.option.1 = First color, then brightness
thing-type.config.dirigera.color-light.fadeTime.label = Fade Time
thing-type.config.dirigera.color-light.fadeTime.description = Required time for fade sequnce to color or brightness
thing-type.config.dirigera.color-light.id.label = Device Id
thing-type.config.dirigera.color-light.id.description = Unique id of this device
thing-type.config.dirigera.gateway.discovery.label = Discovery
thing-type.config.dirigera.gateway.discovery.description = Configure if paired devices shall be detected by discovery
thing-type.config.dirigera.gateway.id.label = Device Id
thing-type.config.dirigera.gateway.id.description = Unique id of this gateway
thing-type.config.dirigera.gateway.ipAddress.label = IP Address
thing-type.config.dirigera.gateway.ipAddress.description = Gateway IP Address
thing-type.config.dirigera.light-device.fadeTime.label = Fade Time
thing-type.config.dirigera.light-device.fadeTime.description = Required time for fade sequnce to color or brightness
thing-type.config.dirigera.light-device.id.label = Device Id
thing-type.config.dirigera.light-device.id.description = Unique id of this device
# channel types
channel-type.dirigera.alarm.label = Alarm Switch
channel-type.dirigera.blind-dimmer.label = Blind Level
channel-type.dirigera.blind-dimmer.description = Current blind level
channel-type.dirigera.blind-state.label = Blind State
channel-type.dirigera.blind-state.description = State if blind is moving up, down or stopped
channel-type.dirigera.blind-state.state.option.0 = Stopped
channel-type.dirigera.blind-state.state.option.1 = Up
channel-type.dirigera.blind-state.state.option.2 = Down
channel-type.dirigera.blind-state.command.option.0 = Stopped
channel-type.dirigera.blind-state.command.option.1 = Up
channel-type.dirigera.blind-state.command.option.2 = Down
channel-type.dirigera.child-lock.label = Child Lock
channel-type.dirigera.child-lock.description = Child lock for button on device
channel-type.dirigera.contact.label = Contact
channel-type.dirigera.custom-name.label = Custom Name
channel-type.dirigera.custom-name.description = Name given from IKEA home smart
channel-type.dirigera.datetime-reset.label = Date Time
channel-type.dirigera.datetime-reset.state.pattern = %1$tA, %1$td.%1$tm. %1$tH:%1$tM
channel-type.dirigera.datetime-reset.command.option.0 = Reset now
channel-type.dirigera.datetime.label = Date Time
channel-type.dirigera.datetime.state.pattern = %1$tA, %1$td.%1$tm. %1$tH:%1$tM
channel-type.dirigera.dimmer.label = Dimmer
channel-type.dirigera.disable-status-light.label = Disable Status Light
channel-type.dirigera.disable-status-light.description = Disable status light on device
channel-type.dirigera.duration.label = Time
channel-type.dirigera.duration.command.option.1 min = 1 minute
channel-type.dirigera.duration.command.option.3 min = 3 minutes
channel-type.dirigera.duration.command.option.5 min = 5 minutes
channel-type.dirigera.duration.command.option.10 min = 10 minutes
channel-type.dirigera.duration.command.option.15 min = 15 minutes
channel-type.dirigera.duration.command.option.20 min = 20 minutes
channel-type.dirigera.duration.command.option.30 min = 30 minutes
channel-type.dirigera.duration.command.option.40 min = 40 minutes
channel-type.dirigera.duration.command.option.60 min = 60 minutes
channel-type.dirigera.fan-mode.label = Fan Mode
channel-type.dirigera.fan-mode.state.option.0 = Auto
channel-type.dirigera.fan-mode.state.option.1 = Low
channel-type.dirigera.fan-mode.state.option.2 = Medium
channel-type.dirigera.fan-mode.state.option.3 = High
channel-type.dirigera.fan-mode.state.option.4 = On
channel-type.dirigera.fan-mode.state.option.5 = Off
channel-type.dirigera.fan-mode.command.option.0 = Auto
channel-type.dirigera.fan-mode.command.option.1 = Low
channel-type.dirigera.fan-mode.command.option.2 = Medium
channel-type.dirigera.fan-mode.command.option.3 = High
channel-type.dirigera.fan-mode.command.option.4 = On
channel-type.dirigera.fan-mode.command.option.5 = Off
channel-type.dirigera.illuminance.label = Illuminance
channel-type.dirigera.illuminance.description = Illuminance in Lux
channel-type.dirigera.image.label = Image
channel-type.dirigera.light-preset.label = Light Preset
channel-type.dirigera.light-preset.command.option.Off = Off
channel-type.dirigera.light-preset.command.option.Warm = Warm
channel-type.dirigera.light-preset.command.option.Slowdown = Slowdown
channel-type.dirigera.light-preset.command.option.Smooth = Smooth
channel-type.dirigera.light-preset.command.option.Bright = Bright
channel-type.dirigera.link-candidates.label = Link Candidates
channel-type.dirigera.link-candidates.description = Candidates which can be linked
channel-type.dirigera.links.label = Links
channel-type.dirigera.links.description = Linked controllers and sensors
channel-type.dirigera.ota-percent.label = OTA Progress
channel-type.dirigera.ota-percent.description = Over-the-air update progress
channel-type.dirigera.ota-state.label = OTA State
channel-type.dirigera.ota-state.description = Over-the-air current state
channel-type.dirigera.ota-state.state.option.0 = Ready to check
channel-type.dirigera.ota-state.state.option.1 = Check in progress
channel-type.dirigera.ota-state.state.option.2 = Ready to download
channel-type.dirigera.ota-state.state.option.3 = Download in progress
channel-type.dirigera.ota-state.state.option.4 = Update in progress
channel-type.dirigera.ota-state.state.option.5 = Update failed
channel-type.dirigera.ota-state.state.option.6 = Ready to update
channel-type.dirigera.ota-state.state.option.7 = Check failed
channel-type.dirigera.ota-state.state.option.8 = Download failed
channel-type.dirigera.ota-state.state.option.9 = Update complete
channel-type.dirigera.ota-state.state.option.10 = Battery check failed
channel-type.dirigera.ota-status.label = OTA Status
channel-type.dirigera.ota-status.description = Over-the-air overall status
channel-type.dirigera.ota-status.state.option.0 = Up to date
channel-type.dirigera.ota-status.state.option.1 = Update available
channel-type.dirigera.pm25.label = Particulate Matter category 2.5
channel-type.dirigera.repeat.label = Repeat Options
channel-type.dirigera.repeat.state.option.0 = Off
channel-type.dirigera.repeat.state.option.1 = Title
channel-type.dirigera.repeat.state.option.2 = Playlist
channel-type.dirigera.repeat.command.option.0 = Off
channel-type.dirigera.repeat.command.option.1 = Title
channel-type.dirigera.repeat.command.option.2 = Playlist
channel-type.dirigera.scene-trigger.label = Scene Trigger
channel-type.dirigera.scene-trigger.command.option.0 = Trigger
channel-type.dirigera.scene-trigger.command.option.1 = Undo
channel-type.dirigera.schedule-end-time.label = Schedule Time
channel-type.dirigera.schedule-end-time.state.pattern = %1$tH:%1$tM
channel-type.dirigera.schedule-end-time.command.option.04:00 = 4:00
channel-type.dirigera.schedule-end-time.command.option.04:30 = 4:30
channel-type.dirigera.schedule-end-time.command.option.05:00 = 5:00
channel-type.dirigera.schedule-end-time.command.option.05:30 = 5:30
channel-type.dirigera.schedule-end-time.command.option.06:00 = 6:00
channel-type.dirigera.schedule-end-time.command.option.06:30 = 6:30
channel-type.dirigera.schedule-end-time.command.option.07:00 = 7:00
channel-type.dirigera.schedule-end-time.command.option.07:30 = 7:30
channel-type.dirigera.schedule-end-time.command.option.08:00 = 8:00
channel-type.dirigera.schedule-start-time.label = Schedule Time
channel-type.dirigera.schedule-start-time.state.pattern = %1$tH:%1$tM
channel-type.dirigera.schedule-start-time.command.option.16:00 = 16:00
channel-type.dirigera.schedule-start-time.command.option.16:30 = 16:30
channel-type.dirigera.schedule-start-time.command.option.17:00 = 17:00
channel-type.dirigera.schedule-start-time.command.option.17:30 = 17:30
channel-type.dirigera.schedule-start-time.command.option.18:00 = 18:00
channel-type.dirigera.schedule-start-time.command.option.18:30 = 18:30
channel-type.dirigera.schedule-start-time.command.option.19:00 = 19:00
channel-type.dirigera.schedule-start-time.command.option.19:30 = 19:30
channel-type.dirigera.schedule-start-time.command.option.20:00 = 20:00
channel-type.dirigera.sensor-schedule.label = Sensor Schedule
channel-type.dirigera.sensor-schedule.state.option.0 = Always
channel-type.dirigera.sensor-schedule.state.option.1 = Follow Sun
channel-type.dirigera.sensor-schedule.state.option.2 = Time schedule
channel-type.dirigera.sensor-schedule.command.option.0 = Always
channel-type.dirigera.sensor-schedule.command.option.1 = Follow Sun
channel-type.dirigera.sensor-schedule.command.option.2 = Time schedule
channel-type.dirigera.startup.label = Startup Behavior
channel-type.dirigera.startup.description = Startup behavior after power cutoff
channel-type.dirigera.startup.state.option.0 = Previous
channel-type.dirigera.startup.state.option.1 = On
channel-type.dirigera.startup.state.option.2 = Off
channel-type.dirigera.startup.state.option.3 = Toggle
channel-type.dirigera.startup.command.option.0 = Previous
channel-type.dirigera.startup.command.option.1 = On
channel-type.dirigera.startup.command.option.2 = Off
channel-type.dirigera.startup.command.option.3 = Toggle
channel-type.dirigera.switch-ro.label = On Off Switch
channel-type.dirigera.switch.label = On Off Switch
channel-type.dirigera.text.label = Simple Text
channel-type.dirigera.time.label = Time
channel-type.dirigera.voc.label = VOC Index
# thing status types
dirigera.device.status.missing-ip = No IP Address configured
dirigera.device.status.wrong-bridge-handler = BridgeHandler isn't a Gateway
dirigera.device.status.missing-bridge-handler = BridgeHandler is missing
dirigera.device.status.missing-bridge = No Bridge configured
dirigera.device.status.not-reachable = Device not reachable
dirigera.device.status.api-error = API {0} cannot be created
dirigera.device.status.id-not-found = Device id {0} not found
dirigera.device.status.ttuid-mismatch = Handler {0} doesn't match with model {1}
dirigera.gateway.status.pairing-button = Press Button on DIRIGERA Gateway
dirigera.gateway.status.pairing-retry = Pairing failed. Stop and start bridge to initialize new pairing.
dirigera.gateway.status.comm-error = Gateway HTTP Status {0}
dirigera.gateway.status.no-gateway = No Gateway found
dirigera.gateway.status.ambiguous-gateway = More than one Gateway found
dirigera.scene.status.scene-not-found = Scene not found

View File

@ -0,0 +1,55 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="dirigera"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<thing-type id="air-purifier">
<supported-bridge-type-refs>
<bridge-type-ref id="gateway"/>
</supported-bridge-type-refs>
<label>Air Purifier</label>
<description>Air cleaning device with particle filter</description>
<channels>
<channel id="fan-mode" typeId="fan-mode">
<label>Fan Mode</label>
<description>Fan on, off, speed or automatic behavior</description>
</channel>
<channel id="fan-speed" typeId="dimmer">
<label>Fan Speed</label>
<description>Manual regulation of fan speed</description>
</channel>
<channel id="fan-runtime" typeId="time">
<label>Fan Runtime</label>
<description>Fan runtime in minutes</description>
</channel>
<channel id="filter-elapsed" typeId="time">
<label>Filter Elapsed</label>
<description>Filter elapsed time in minutes</description>
</channel>
<channel id="filter-remain" typeId="time">
<label>Filter Remain</label>
<description>Remaining filter time in minutes</description>
</channel>
<channel id="filter-lifetime" typeId="time">
<label>Filter Lifetime</label>
<description>Filter lifetime in minutes</description>
</channel>
<channel id="filter-alarm" typeId="switch-ro">
<label>Filter Alarm</label>
<description>Filter alarm signal</description>
</channel>
<channel id="particulate-matter" typeId="pm25">
<label>Particulate Matter</label>
<description>Category 2.5 particulate matter</description>
</channel>
<channel id="disable-status-light" typeId="disable-status-light"/>
<channel id="child-lock" typeId="child-lock"/>
<channel id="custom-name" typeId="custom-name"/>
</channels>
<config-description-ref uri="thing-type:dirigera:base-device"/>
</thing-type>
</thing:thing-descriptions>

View File

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="dirigera"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<thing-type id="air-quality">
<supported-bridge-type-refs>
<bridge-type-ref id="gateway"/>
</supported-bridge-type-refs>
<label>Air Quality</label>
<description>Air measure for temperature, humidity and particles</description>
<channels>
<channel id="temperature" typeId="system.indoor-temperature">
<label>Temperature</label>
<description>Current indoor temperature</description>
</channel>
<channel id="humidity" typeId="system.atmospheric-humidity">
<label>Humidity</label>
<description>Atmospheric humidity in percent</description>
</channel>
<channel id="particulate-matter" typeId="pm25">
<label>Particulate Matter</label>
<description>Category 2.5 particulate matter</description>
</channel>
<channel id="voc-index" typeId="voc">
<label>VOC Index</label>
<description>Relative VOC intensity compared to recent history</description>
</channel>
<channel id="custom-name" typeId="custom-name"/>
</channels>
<config-description-ref uri="thing-type:dirigera:base-device"/>
</thing-type>
</thing:thing-descriptions>

View File

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="dirigera"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<thing-type id="blind-controller">
<supported-bridge-type-refs>
<bridge-type-ref id="gateway"/>
</supported-bridge-type-refs>
<label>Blinds Controller</label>
<description>Controller to open and close blinds</description>
<channels>
<channel id="battery-level" typeId="system.battery-level">
<label>Battery Charge Level</label>
<description>Battery charge level in percent</description>
</channel>
<channel id="custom-name" typeId="custom-name"/>
</channels>
<config-description-ref uri="thing-type:dirigera:base-device"/>
</thing-type>
</thing:thing-descriptions>

View File

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="dirigera"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<thing-type id="blind">
<supported-bridge-type-refs>
<bridge-type-ref id="gateway"/>
</supported-bridge-type-refs>
<label>Blind</label>
<description>Window or door blind</description>
<channels>
<channel id="blind-state" typeId="blind-state"/>
<channel id="blind-level" typeId="blind-dimmer"/>
<channel id="battery-level" typeId="system.battery-level">
<label>Battery Charge Level</label>
<description>Battery charge level in percent</description>
</channel>
<channel id="custom-name" typeId="custom-name"/>
</channels>
<config-description-ref uri="thing-type:dirigera:base-device"/>
</thing-type>
</thing:thing-descriptions>

View File

@ -0,0 +1,347 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="dirigera"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<channel-type id="text">
<item-type>String</item-type>
<label>Simple Text</label>
</channel-type>
<channel-type id="custom-name">
<item-type>String</item-type>
<label>Custom Name</label>
<description>Name given from IKEA home smart</description>
</channel-type>
<channel-type id="light-preset">
<item-type>String</item-type>
<label>Light Preset</label>
<command>
<options>
<option value="Off">Off</option>
<option value="Warm">Warm</option>
<option value="Slowdown">Slowdown</option>
<option value="Smooth">Smooth</option>
<option value="Bright">Bright</option>
</options>
</command>
</channel-type>
<channel-type id="sensor-schedule">
<item-type>Number</item-type>
<label>Sensor Schedule</label>
<state>
<options>
<option value="0">Always</option>
<option value="1">Follow Sun</option>
<option value="2">Time schedule</option>
</options>
</state>
<command>
<options>
<option value="0">Always</option>
<option value="1">Follow Sun</option>
<option value="2">Time schedule</option>
</options>
</command>
<autoUpdatePolicy>veto</autoUpdatePolicy>
</channel-type>
<channel-type id="repeat">
<item-type>Number</item-type>
<label>Repeat Options</label>
<state>
<options>
<option value="0">Off</option>
<option value="1">Title</option>
<option value="2">Playlist</option>
</options>
</state>
<command>
<options>
<option value="0">Off</option>
<option value="1">Title</option>
<option value="2">Playlist</option>
</options>
</command>
<autoUpdatePolicy>veto</autoUpdatePolicy>
</channel-type>
<channel-type id="startup">
<item-type>Number</item-type>
<label>Startup Behavior</label>
<description>Startup behavior after power cutoff</description>
<state>
<options>
<option value="0">Previous</option>
<option value="1">On</option>
<option value="2">Off</option>
<option value="3">Toggle</option>
</options>
</state>
<command>
<options>
<option value="0">Previous</option>
<option value="1">On</option>
<option value="2">Off</option>
<option value="3">Toggle</option>
</options>
</command>
<autoUpdatePolicy>veto</autoUpdatePolicy>
</channel-type>
<channel-type id="disable-status-light">
<item-type>Switch</item-type>
<label>Disable Status Light</label>
<description>Disable status light on device</description>
<autoUpdatePolicy>veto</autoUpdatePolicy>
</channel-type>
<channel-type id="child-lock">
<item-type>Switch</item-type>
<label>Child Lock</label>
<description>Child lock for button on device</description>
<autoUpdatePolicy>veto</autoUpdatePolicy>
</channel-type>
<channel-type id="switch">
<item-type>Switch</item-type>
<label>On Off Switch</label>
<autoUpdatePolicy>veto</autoUpdatePolicy>
</channel-type>
<channel-type id="switch-ro">
<item-type>Switch</item-type>
<label>On Off Switch</label>
<state readOnly="true"/>
</channel-type>
<channel-type id="alarm">
<item-type>Switch</item-type>
<label>Alarm Switch</label>
<category>Alarm</category>
<tags>
<tag>Alarm</tag>
<tag>Water</tag>
</tags>
<state readOnly="true"/>
</channel-type>
<channel-type id="illuminance">
<item-type>Number:Illuminance</item-type>
<label>Illuminance</label>
<description>Illuminance in Lux </description>
<tags>
<tag>Measurement</tag>
<tag>Light</tag>
</tags>
<state pattern="%.0f %unit%" readOnly="true"/>
</channel-type>
<channel-type id="contact">
<item-type>Contact</item-type>
<label>Contact</label>
<category>Contact</category>
<tags>
<tag>OpenState</tag>
</tags>
<state readOnly="true"/>
</channel-type>
<channel-type id="dimmer">
<item-type>Dimmer</item-type>
<label>Dimmer</label>
<autoUpdatePolicy>veto</autoUpdatePolicy>
</channel-type>
<channel-type id="image">
<item-type>Image</item-type>
<label>Image</label>
<state readOnly="true"/>
</channel-type>
<channel-type id="schedule-start-time">
<item-type>DateTime</item-type>
<label>Schedule Time</label>
<state pattern="%1$tH:%1$tM"/>
<command>
<options>
<option value="16:00">16:00</option>
<option value="16:30">16:30</option>
<option value="17:00">17:00</option>
<option value="17:30">17:30</option>
<option value="18:00">18:00</option>
<option value="18:30">18:30</option>
<option value="19:00">19:00</option>
<option value="19:30">19:30</option>
<option value="20:00">20:00</option>
</options>
</command>
</channel-type>
<channel-type id="schedule-end-time">
<item-type>DateTime</item-type>
<label>Schedule Time</label>
<state pattern="%1$tH:%1$tM"/>
<command>
<options>
<option value="04:00">4:00</option>
<option value="04:30">4:30</option>
<option value="05:00">5:00</option>
<option value="05:30">5:30</option>
<option value="06:00">6:00</option>
<option value="06:30">6:30</option>
<option value="07:00">7:00</option>
<option value="07:30">7:30</option>
<option value="08:00">8:00</option>
</options>
</command>
<autoUpdatePolicy>veto</autoUpdatePolicy>
</channel-type>
<channel-type id="datetime">
<item-type>DateTime</item-type>
<label>Date Time</label>
<state pattern="%1$tA, %1$td.%1$tm. %1$tH:%1$tM"/>
</channel-type>
<channel-type id="datetime-reset">
<item-type>DateTime</item-type>
<label>Date Time</label>
<state pattern="%1$tA, %1$td.%1$tm. %1$tH:%1$tM"/>
<command>
<options>
<option value="0">Reset now</option>
</options>
</command>
<autoUpdatePolicy>veto</autoUpdatePolicy>
</channel-type>
<channel-type id="scene-trigger">
<item-type>Number</item-type>
<label>Scene Trigger</label>
<command>
<options>
<option value="0">Trigger</option>
<option value="1">Undo</option>
</options>
</command>
<autoUpdatePolicy>veto</autoUpdatePolicy>
</channel-type>
<channel-type id="pm25">
<item-type unitHint="µg/m³">Number:Density</item-type>
<label>Particulate Matter category 2.5</label>
<state pattern="%.0f %unit%" readOnly="true"/>
</channel-type>
<channel-type id="voc">
<item-type>Number</item-type>
<label>VOC Index</label>
<state readOnly="true"/>
</channel-type>
<channel-type id="time">
<item-type unitHint="min">Number:Time</item-type>
<label>Time</label>
</channel-type>
<channel-type id="duration">
<item-type unitHint="min">Number:Time</item-type>
<label>Time</label>
<command>
<options>
<option value="1 min">1 minute</option>
<option value="3 min">3 minutes</option>
<option value="5 min">5 minutes</option>
<option value="10 min">10 minutes</option>
<option value="15 min">15 minutes</option>
<option value="20 min">20 minutes</option>
<option value="30 min">30 minutes</option>
<option value="40 min">40 minutes</option>
<option value="60 min">60 minutes</option>
</options>
</command>
</channel-type>
<channel-type id="fan-mode">
<item-type>Number</item-type>
<label>Fan Mode</label>
<state>
<options>
<option value="0">Auto</option>
<option value="1">Low</option>
<option value="2">Medium</option>
<option value="3">High</option>
<option value="4">On</option>
<option value="5">Off</option>
</options>
</state>
<command>
<options>
<option value="0">Auto</option>
<option value="1">Low</option>
<option value="2">Medium</option>
<option value="3">High</option>
<option value="4">On</option>
<option value="5">Off</option>
</options>
</command>
</channel-type>
<channel-type id="blind-state">
<item-type>Number</item-type>
<label>Blind State</label>
<description>State if blind is moving up, down or stopped</description>
<state>
<options>
<option value="0">Stopped</option>
<option value="1">Up</option>
<option value="2">Down</option>
</options>
</state>
<command>
<options>
<option value="0">Stopped</option>
<option value="1">Up</option>
<option value="2">Down</option>
</options>
</command>
</channel-type>
<channel-type id="blind-dimmer">
<item-type>Dimmer</item-type>
<label>Blind Level</label>
<description>Current blind level</description>
<category>Rollershutter</category>
<tags>
<tag>OpenLevel</tag>
</tags>
<autoUpdatePolicy>veto</autoUpdatePolicy>
</channel-type>
<channel-type id="ota-status">
<item-type>Number</item-type>
<label>OTA Status</label>
<description>Over-the-air overall status</description>
<state readOnly="true">
<options>
<option value="0">Up to date</option>
<option value="1">Update available</option>
</options>
</state>
</channel-type>
<channel-type id="ota-state" advanced="true">
<item-type>Number</item-type>
<label>OTA State</label>
<description>Over-the-air current state</description>
<state readOnly="true">
<options>
<option value="0">Ready to check</option>
<option value="1">Check in progress</option>
<option value="2">Ready to download</option>
<option value="3">Download in progress</option>
<option value="4">Update in progress</option>
<option value="5">Update failed</option>
<option value="6">Ready to update</option>
<option value="7">Check failed</option>
<option value="8">Download failed</option>
<option value="9">Update complete</option>
<option value="10">Battery check failed</option>
</options>
</state>
</channel-type>
<channel-type id="ota-percent" advanced="true">
<item-type unitHint="%">Number:Dimensionless</item-type>
<label>OTA Progress</label>
<description>Over-the-air update progress</description>
<state pattern="%.0f %unit%" readOnly="true"/>
</channel-type>
<channel-type id="links" advanced="true">
<item-type>String</item-type>
<label>Links</label>
<description>Linked controllers and sensors</description>
<autoUpdatePolicy>veto</autoUpdatePolicy>
</channel-type>
<channel-type id="link-candidates" advanced="true">
<item-type>String</item-type>
<label>Link Candidates</label>
<description>Candidates which can be linked</description>
<autoUpdatePolicy>veto</autoUpdatePolicy>
</channel-type>
</thing:thing-descriptions>

View File

@ -0,0 +1,47 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="dirigera"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<thing-type id="color-light">
<supported-bridge-type-refs>
<bridge-type-ref id="gateway"/>
</supported-bridge-type-refs>
<label>Color Light</label>
<description>Light with color support</description>
<channels>
<channel id="power" typeId="system.power">
<label>Light Powered</label>
<description>Power state of light</description>
<autoUpdatePolicy>veto</autoUpdatePolicy>
</channel>
<channel id="brightness" typeId="system.brightness">
<label>Brightness</label>
<description>Brightness of light in percent</description>
<autoUpdatePolicy>veto</autoUpdatePolicy>
</channel>
<channel id="color-temperature" typeId="system.color-temperature">
<label>Color Temperature</label>
<description>Color temperature from cold (0 %) to warm (100 %)</description>
<autoUpdatePolicy>veto</autoUpdatePolicy>
</channel>
<channel id="color-temperature-abs" typeId="system.color-temperature-abs">
<label>Color Temperature Kelvin</label>
<description>Color temperature of a bulb in Kelvin</description>
<autoUpdatePolicy>veto</autoUpdatePolicy>
</channel>
<channel id="color" typeId="system.color">
<label>Color</label>
<description>Color of light with hue, saturation and brightness</description>
<autoUpdatePolicy>veto</autoUpdatePolicy>
</channel>
<channel id="startup" typeId="startup"/>
<channel id="custom-name" typeId="custom-name"/>
</channels>
<config-description-ref uri="thing-type:dirigera:color-light"/>
</thing-type>
</thing:thing-descriptions>

View File

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="dirigera"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<thing-type id="contact-sensor">
<supported-bridge-type-refs>
<bridge-type-ref id="gateway"/>
</supported-bridge-type-refs>
<label>Contact Sensor</label>
<description>Sensor tracking if windows or doors are open</description>
<channels>
<channel id="contact" typeId="contact">
<label>Contact State</label>
<description>State if door or window is open or closed</description>
</channel>
<channel id="battery-level" typeId="system.battery-level">
<label>Battery Charge Level</label>
<description>Battery charge level in percent</description>
</channel>
<channel id="custom-name" typeId="custom-name"/>
</channels>
<config-description-ref uri="thing-type:dirigera:base-device"/>
</thing-type>
</thing:thing-descriptions>

View File

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="dirigera"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<thing-type id="dimmable-light">
<supported-bridge-type-refs>
<bridge-type-ref id="gateway"/>
</supported-bridge-type-refs>
<label>Dimmable Light</label>
<description>Light with brightness support</description>
<channels>
<channel id="power" typeId="system.power">
<label>Light Powered</label>
<description>Power state of light</description>
<autoUpdatePolicy>veto</autoUpdatePolicy>
</channel>
<channel id="brightness" typeId="system.brightness">
<label>Brightness</label>
<description>Brightness of light in percent</description>
<autoUpdatePolicy>veto</autoUpdatePolicy>
</channel>
<channel id="startup" typeId="startup"/>
<channel id="custom-name" typeId="custom-name"/>
</channels>
<config-description-ref uri="thing-type:dirigera:light-device"/>
</thing-type>
</thing:thing-descriptions>

View File

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="dirigera"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<thing-type id="double-shortcut">
<supported-bridge-type-refs>
<bridge-type-ref id="gateway"/>
</supported-bridge-type-refs>
<label>Two Button Shortcut</label>
<description>Shortcut controller with two buttons</description>
<channels>
<channel id="button1" typeId="system.button">
<label>Button 1 Trigger</label>
<description>Trigger of first button</description>
</channel>
<channel id="button2" typeId="system.button">
<label>Button 2 Trigger</label>
<description>Trigger of second button</description>
</channel>
<channel id="battery-level" typeId="system.battery-level">
<label>Battery Charge Level</label>
<description>Battery charge level in percent</description>
</channel>
<channel id="custom-name" typeId="custom-name"/>
</channels>
<config-description-ref uri="thing-type:dirigera:base-device"/>
</thing-type>
</thing:thing-descriptions>

View File

@ -0,0 +1,49 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="dirigera"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<bridge-type id="gateway">
<label>DIRIGERA Gateway</label>
<description>IKEA Gateway for smart products</description>
<channels>
<channel id="custom-name" typeId="custom-name"/>
<channel id="location" typeId="text">
<label>Home Location</label>
<description>Location in latitude, longitude coordinates</description>
</channel>
<channel id="sunrise" typeId="datetime">
<label>Sunrise</label>
<description>Date and time of next sunrise</description>
</channel>
<channel id="sunset" typeId="datetime">
<label>Sunset</label>
<description>Date and time of next sunset</description>
</channel>
<channel id="pairing" typeId="switch">
<label>Pairing</label>
<description>Sets DIRIGERA hub into pairing mode</description>
</channel>
<channel id="ota-status" typeId="ota-status">
<label>OTA Status</label>
<description>Over-the-air overall status</description>
</channel>
<channel id="ota-state" typeId="ota-state">
<label>OTA State</label>
<description>Over-the-air current state</description>
</channel>
<channel id="ota-progress" typeId="ota-percent">
<label>OTA Progress</label>
<description>Over-the-air update progress</description>
</channel>
<channel id="statistics" typeId="text">
<label>Gateway Statistics</label>
<description>Several statistics about gateway activities</description>
</channel>
</channels>
<config-description-ref uri="thing-type:dirigera:gateway"/>
</bridge-type>
</thing:thing-descriptions>

View File

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="dirigera"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<thing-type id="light-controller">
<supported-bridge-type-refs>
<bridge-type-ref id="gateway"/>
</supported-bridge-type-refs>
<label>Light Controller</label>
<description>Controller to handle light attributes</description>
<channels>
<channel id="light-preset" typeId="light-preset">
<label>Light Preset</label>
<description>Light presets for different times of the day</description>
</channel>
<channel id="battery-level" typeId="system.battery-level">
<label>Battery Charge Level</label>
<description>Battery charge level in percent</description>
</channel>
<channel id="custom-name" typeId="custom-name"/>
</channels>
<config-description-ref uri="thing-type:dirigera:base-device"/>
</thing-type>
</thing:thing-descriptions>

View File

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="dirigera"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<thing-type id="light-sensor">
<supported-bridge-type-refs>
<bridge-type-ref id="gateway"/>
</supported-bridge-type-refs>
<label>Light Sensor</label>
<description>Sensor measuring illuminance in your room</description>
<channels>
<channel id="illuminance" typeId="illuminance"/>
</channels>
<config-description-ref uri="thing-type:dirigera:base-device"/>
</thing-type>
</thing:thing-descriptions>

View File

@ -0,0 +1,53 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="dirigera"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<thing-type id="motion-light-sensor">
<supported-bridge-type-refs>
<bridge-type-ref id="gateway"/>
</supported-bridge-type-refs>
<label>Motion Light Sensor</label>
<description>Sensor detecting motion events and measures light level</description>
<channels>
<channel id="motion" typeId="system.motion">
<label>Motion Detected</label>
<description>Motion detected by the device</description>
</channel>
<channel id="active-duration" typeId="duration">
<label>Active Duration</label>
<description>Keep connected devices active for this duration</description>
</channel>
<channel id="illuminance" typeId="illuminance">
<label>Illuminance</label>
<description>Illuminance in Lux</description>
</channel>
<channel id="battery-level" typeId="system.battery-level">
<label>Battery Charge Level</label>
<description>Battery charge level in percent</description>
</channel>
<channel id="schedule" typeId="sensor-schedule">
<label>Activity Schedule</label>
<description>Schedule when the sensor shall be active</description>
</channel>
<channel id="schedule-start" typeId="schedule-start-time">
<label>Activity Schedule Start</label>
<description>Start time of sensor activity</description>
</channel>
<channel id="schedule-end" typeId="schedule-end-time">
<label>Activity Schedule End</label>
<description>End time of sensor activity</description>
</channel>
<channel id="light-preset" typeId="light-preset">
<label>Light Preset</label>
<description>Light presets for different times of the day</description>
</channel>
<channel id="custom-name" typeId="custom-name"/>
</channels>
<config-description-ref uri="thing-type:dirigera:base-device"/>
</thing-type>
</thing:thing-descriptions>

View File

@ -0,0 +1,49 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="dirigera"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<thing-type id="motion-sensor">
<supported-bridge-type-refs>
<bridge-type-ref id="gateway"/>
</supported-bridge-type-refs>
<label>Motion Sensor</label>
<description>Sensor detecting motion events</description>
<channels>
<channel id="motion" typeId="system.motion">
<label>Detection Flag</label>
<description>Flag if detection happened</description>
</channel>
<channel id="active-duration" typeId="duration">
<label>Active Duration</label>
<description>Keep connected devices active for this duration</description>
</channel>
<channel id="battery-level" typeId="system.battery-level">
<label>Battery Charge Level</label>
<description>Battery charge level in percent</description>
</channel>
<channel id="schedule" typeId="sensor-schedule">
<label>Activity Schedule</label>
<description>Schedule when the sensor shall be active</description>
</channel>
<channel id="schedule-start" typeId="schedule-start-time">
<label>Activity Schedule Start</label>
<description>Start time of sensor activity</description>
</channel>
<channel id="schedule-end" typeId="schedule-end-time">
<label>Activity Schedule End</label>
<description>End time of sensor activity</description>
</channel>
<channel id="light-preset" typeId="light-preset">
<label>Light Preset</label>
<description>Light presets for different times of the day</description>
</channel>
<channel id="custom-name" typeId="custom-name"/>
</channels>
<config-description-ref uri="thing-type:dirigera:base-device"/>
</thing-type>
</thing:thing-descriptions>

View File

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="dirigera"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<thing-type id="power-plug">
<supported-bridge-type-refs>
<bridge-type-ref id="gateway"/>
</supported-bridge-type-refs>
<label>Power Plug</label>
<description>Power plug with control of power state, startup behavior, hardware on/off button and status light</description>
<channels>
<channel id="power" typeId="system.power">
<label>Plug Powered</label>
<description>Power state of plug </description>
<autoUpdatePolicy>veto</autoUpdatePolicy>
</channel>
<channel id="disable-status-light" typeId="disable-status-light"/>
<channel id="child-lock" typeId="child-lock"/>
<channel id="startup" typeId="startup"/>
<channel id="custom-name" typeId="custom-name"/>
</channels>
<config-description-ref uri="thing-type:dirigera:base-device"/>
</thing-type>
</thing:thing-descriptions>

View File

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="dirigera"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<thing-type id="repeater">
<supported-bridge-type-refs>
<bridge-type-ref id="gateway"/>
</supported-bridge-type-refs>
<label>Repeater</label>
<description>Repeater to strengthen signal</description>
<channels>
<channel id="custom-name" typeId="custom-name"/>
</channels>
<config-description-ref uri="thing-type:dirigera:base-device"/>
</thing-type>
</thing:thing-descriptions>

View File

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="dirigera"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<thing-type id="scene">
<supported-bridge-type-refs>
<bridge-type-ref id="gateway"/>
</supported-bridge-type-refs>
<label>Scene</label>
<description>Scene from IKEA home smart App which can be triggered</description>
<channels>
<channel id="trigger" typeId="scene-trigger">
<label>Scene Trigger</label>
<description>Perform / undo scene execution </description>
</channel>
<channel id="last-trigger" typeId="datetime">
<label>Last Trigger</label>
<description>Date and time when last trigger occurred</description>
</channel>
</channels>
<config-description-ref uri="thing-type:dirigera:base-device"/>
</thing-type>
</thing:thing-descriptions>

View File

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="dirigera"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<thing-type id="simple-plug">
<supported-bridge-type-refs>
<bridge-type-ref id="gateway"/>
</supported-bridge-type-refs>
<label>Simple Plug</label>
<description>Simple plug with control of power state and startup behavior</description>
<channels>
<channel id="power" typeId="system.power">
<label>Plug Powered</label>
<description>Power state of plug </description>
<autoUpdatePolicy>veto</autoUpdatePolicy>
</channel>
<channel id="startup" typeId="startup"/>
<channel id="custom-name" typeId="custom-name"/>
</channels>
<config-description-ref uri="thing-type:dirigera:base-device"/>
</thing-type>
</thing:thing-descriptions>

View File

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="dirigera"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<thing-type id="single-shortcut">
<supported-bridge-type-refs>
<bridge-type-ref id="gateway"/>
</supported-bridge-type-refs>
<label>Single Button Shortcut</label>
<description>Shortcut controller with one button</description>
<channels>
<channel id="button1" typeId="system.button">
<label>Button 1 Trigger</label>
<description>Trigger of first button</description>
</channel>
<channel id="battery-level" typeId="system.battery-level">
<label>Battery Charge Level</label>
<description>Battery charge level in percent</description>
</channel>
<channel id="custom-name" typeId="custom-name"/>
</channels>
<config-description-ref uri="thing-type:dirigera:base-device"/>
</thing-type>
</thing:thing-descriptions>

View File

@ -0,0 +1,53 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="dirigera"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<thing-type id="smart-plug">
<supported-bridge-type-refs>
<bridge-type-ref id="gateway"/>
</supported-bridge-type-refs>
<label>Smart Power Plug</label>
<description>Power plug with electricity measurements</description>
<channels>
<channel id="power" typeId="system.power">
<label>Plug Powered</label>
<description>Power state of plug </description>
<autoUpdatePolicy>veto</autoUpdatePolicy>
</channel>
<channel id="disable-status-light" typeId="disable-status-light"/>
<channel id="child-lock" typeId="child-lock"/>
<channel id="electric-power" typeId="system.electric-power">
<label>Electric Power</label>
<description>Electric power delivered by plug</description>
</channel>
<channel id="energy-total" typeId="system.electric-energy">
<label>Total Energy</label>
<description>Total energy consumption</description>
</channel>
<channel id="energy-reset" typeId="system.electric-energy">
<label>Energy since Reset</label>
<description>Energy consumption since last reset</description>
</channel>
<channel id="reset-date" typeId="datetime-reset">
<label>Reset Date Time</label>
<description>Date and time of last reset</description>
</channel>
<channel id="electric-current" typeId="system.electric-current">
<label>Plug Current</label>
<description>Electric current measured by plug</description>
</channel>
<channel id="electric-voltage" typeId="system.electric-voltage">
<label>Plug Voltage</label>
<description>Electric potential of plug</description>
</channel>
<channel id="startup" typeId="startup"/>
<channel id="custom-name" typeId="custom-name"/>
</channels>
<config-description-ref uri="thing-type:dirigera:base-device"/>
</thing-type>
</thing:thing-descriptions>

View File

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="dirigera"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<thing-type id="sound-controller">
<supported-bridge-type-refs>
<bridge-type-ref id="gateway"/>
</supported-bridge-type-refs>
<label>Sound Controller</label>
<description>Controller for speakers</description>
<channels>
<channel id="battery-level" typeId="system.battery-level">
<label>Battery Charge Level</label>
<description>Battery charge level in percent</description>
</channel>
<channel id="custom-name" typeId="custom-name"/>
</channels>
<config-description-ref uri="thing-type:dirigera:base-device"/>
</thing-type>
</thing:thing-descriptions>

View File

@ -0,0 +1,53 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="dirigera"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<thing-type id="speaker">
<supported-bridge-type-refs>
<bridge-type-ref id="gateway"/>
</supported-bridge-type-refs>
<label>Speaker</label>
<description>Speaker with player activities</description>
<channels>
<channel id="media-control" typeId="system.media-control">
<label>Media Control</label>
<description>Media control play, pause, next, previous</description>
</channel>
<channel id="volume" typeId="system.volume">
<label>Volume Control</label>
<description>Control volume in percent</description>
</channel>
<channel id="mute" typeId="system.mute">
<label>Mute Control</label>
<description>Mute current audio without stop playing</description>
</channel>
<channel id="shuffle" typeId="switch">
<label>Shuffle</label>
<description>Control shuffle mode</description>
</channel>
<channel id="crossfade" typeId="switch">
<label>Cross Fade</label>
<description>Cross fading between tracks</description>
</channel>
<channel id="repeat" typeId="repeat">
<label>Repeat</label>
<description>Repeat Mode</description>
</channel>
<channel id="media-title" typeId="system.media-title">
<label>Media Title</label>
<description>Title of a played media file</description>
</channel>
<channel id="image" typeId="image">
<label>Image</label>
<description>Current playing track image</description>
</channel>
<channel id="custom-name" typeId="custom-name"/>
</channels>
<config-description-ref uri="thing-type:dirigera:base-device"/>
</thing-type>
</thing:thing-descriptions>

View File

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="dirigera"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<thing-type id="switch-light">
<supported-bridge-type-refs>
<bridge-type-ref id="gateway"/>
</supported-bridge-type-refs>
<label>Switch Light</label>
<description>Light with switch ON, OFF capability</description>
<channels>
<channel id="power" typeId="system.power">
<label>Light Powered</label>
<description>Power state of light</description>
</channel>
<channel id="startup" typeId="startup"/>
<channel id="custom-name" typeId="custom-name"/>
</channels>
<config-description-ref uri="thing-type:dirigera:base-device"/>
</thing-type>
</thing:thing-descriptions>

View File

@ -0,0 +1,42 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="dirigera"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<thing-type id="temperature-light">
<supported-bridge-type-refs>
<bridge-type-ref id="gateway"/>
</supported-bridge-type-refs>
<label>Temperature Light</label>
<description>Light with color temperature support</description>
<channels>
<channel id="power" typeId="system.power">
<label>Light Powered</label>
<description>Power state of light</description>
<autoUpdatePolicy>veto</autoUpdatePolicy>
</channel>
<channel id="brightness" typeId="system.brightness">
<label>Light Brightness</label>
<description>Brightness of light in percent</description>
<autoUpdatePolicy>veto</autoUpdatePolicy>
</channel>
<channel id="color-temperature" typeId="system.color-temperature">
<label>Color Temperature</label>
<description>Color temperature from cold (0 %) to warm (100 %)</description>
<autoUpdatePolicy>veto</autoUpdatePolicy>
</channel>
<channel id="color-temperature-abs" typeId="system.color-temperature-abs">
<label>Color Temperature Kelvin</label>
<description>Color temperature of a bulb in Kelvin</description>
<autoUpdatePolicy>veto</autoUpdatePolicy>
</channel>
<channel id="startup" typeId="startup"/>
<channel id="custom-name" typeId="custom-name"/>
</channels>
<config-description-ref uri="thing-type:dirigera:color-light"/>
</thing-type>
</thing:thing-descriptions>

View File

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="dirigera"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<thing-type id="water-sensor">
<supported-bridge-type-refs>
<bridge-type-ref id="gateway"/>
</supported-bridge-type-refs>
<label>Water Sensor</label>
<description>Sensor to detect water leaks</description>
<channels>
<channel id="leak" typeId="alarm">
<label>Leak Detection</label>
<description>Water leak detection</description>
</channel>
<channel id="battery-level" typeId="system.battery-level">
<label>Battery Charge Level</label>
<description>Battery charge level in percent</description>
</channel>
<channel id="custom-name" typeId="custom-name"/>
</channels>
<config-description-ref uri="thing-type:dirigera:base-device"/>
</thing-type>
</thing:thing-descriptions>

View File

@ -0,0 +1,8 @@
{
"attributes": {
"coordinates": {
"latitude": %s,
"longitude": %s,
}
}
}

View File

@ -0,0 +1,5 @@
{
"attributes": {
"coordinates": {}
}
}

View File

@ -0,0 +1,32 @@
[
{
"startTime": "07:00",
"lightLevel": 25,
"colorTemperature": 3000
},
{
"startTime": "09:00",
"lightLevel": 75,
"colorTemperature": 3900
},
{
"startTime": "13:00",
"lightLevel": 100,
"colorTemperature": 4000
},
{
"startTime": "17:00",
"lightLevel": 70,
"colorTemperature": 4000
},
{
"startTime": "20:00",
"lightLevel": 20,
"colorTemperature": 3900
},
{
"startTime": "22:00",
"lightLevel": 1,
"colorTemperature": 3000
}
]

View File

@ -0,0 +1,33 @@
[
{
"startTime": "05:00",
"lightLevel": 100,
"colorTemperature": 4000
},
{
"startTime": "10:00",
"lightLevel": 100,
"colorTemperature": 3800
},
{
"startTime": "17:00",
"lightLevel": 75,
"colorTemperature": 3500
},
{
"startTime": "20:00",
"lightLevel": 45,
"colorTemperature": 3000
},
{
"startTime": "22:00",
"lightLevel": 20,
"colorTemperature": 2400
},
{
"startTime": "23:00",
"lightLevel": 1,
"colorTemperature": 2200
}
]

View File

@ -0,0 +1,32 @@
[
{
"startTime": "06:00",
"lightLevel": 50,
"colorTemperature": 2400
},
{
"startTime": "10:00",
"lightLevel": 100,
"colorTemperature": 4000
},
{
"startTime": "16:00",
"lightLevel": 100,
"colorTemperature": 3500
},
{
"startTime": "20:00",
"lightLevel": 50,
"colorTemperature": 3000
},
{
"startTime": "22:00",
"lightLevel": 20,
"colorTemperature": 2300
},
{
"startTime": "23:00",
"lightLevel": 1,
"colorTemperature": 2300
}
]

View File

@ -0,0 +1,32 @@
[
{
"startTime": "07:00",
"lightLevel": 25,
"colorTemperature": 3000
},
{
"startTime": "09:00",
"lightLevel": 75,
"colorTemperature": 3600
},
{
"startTime": "13:00",
"lightLevel": 100,
"colorTemperature": 4000
},
{
"startTime": "17:00",
"lightLevel": 70,
"colorTemperature": 3600
},
{
"startTime": "20:00",
"lightLevel": 20,
"colorTemperature": 3000
},
{
"startTime": "22:00",
"lightLevel": 1,
"colorTemperature": 2200
}
]

View File

@ -0,0 +1,22 @@
{
"id": "%s",
"type": "customScene",
"info": {
"name": "%s",
"icon": "scenes_home_filled"
},
"triggers": [
{
"type": "controller",
"trigger": {
"controllerType": "shortcutController",
"clickPattern": "%s",
"buttonIndex": %s,
"deviceId": "%s"
}
}
],
"actions": [],
"commands": []
}

View File

@ -0,0 +1,7 @@
{
"attributes": {
"sensorConfig": {
"scheduleOn": false
}
}
}

Some files were not shown because too many files have changed in this diff Show More