added migrated 2.x add-ons

Signed-off-by: Kai Kreuzer <kai@openhab.org>
This commit is contained in:
Kai Kreuzer
2020-09-21 01:58:32 +02:00
parent bbf1a7fd29
commit 6df6783b60
11662 changed files with 1302875 additions and 11 deletions

View File

@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="src" output="target/classes" path="src/main/java">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry excluding="**" kind="src" output="target/classes" path="src/main/resources">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="src" output="target/test-classes" path="src/test/java">
<attributes>
<attribute name="test" value="true"/>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-11">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="output" path="target/classes"/>
</classpath>

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>org.openhab.binding.shelly</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.m2e.core.maven2Builder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.jdt.core.javanature</nature>
<nature>org.eclipse.m2e.core.maven2Nature</nature>
</natures>
</projectDescription>

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,887 @@
# Shelly Binding
This Binding integrates [Alterco Shelly devices](https://shelly.cloud).
Alterco provides a rich set of smart home devices. All of them are WiFi enabled (2,4GHz, IPv4 only) and provide a documented API.
The binding is officially acknowledged by Alterco and openHAB is listed as a reference.
Alterco directly supports this project.
The binding controls the devices independently from the Alterco Shelly Cloud (in fact it can be disabled).
The binding co-exists with Shelly App for Smartphones, Shelly Web App, Shelly Cloud, mqqt and other 3rd party Apps.
The binding focuses on reporting the device status and device control.
Initial setup and device configuration has to be performed using the Shelly Apps.
The binding gets in sync with the next status refresh.
## Supported Devices
| Thing Type | Model | Vendor ID |
|--------------------|--------------------------------------------------------|-----------|
| shelly1 | Shelly Single Relay Switch | SHSW-1 |
| shelly1pm | Shelly Single Relay Switch with integrated Power Meter | SHSW-PM |
| shelly2-relay | Shelly Double Relay Switch in relay mode | SHSW-21 |
| shelly2-roller | Shelly2 in Roller Mode | SHSW-21 |
| shelly25-relay | Shelly 2.5 in Relay Switch | SHSW-25 |
| shelly25-roller | Shelly 2.5 in Roller Mode | SHSW-25 |
| shelly4pro | Shelly 4x Relay Switch | SHSW-44 |
| shellydimmer | Shelly Dimmer | SHDM-1 |
| shellydimmer2 | Shelly Dimmer2 | SHDM-2 |
| shellyix3 | Shelly ix3 | SHIX3-1 |
| shellyplug | Shelly Plug | SHPLG2-1 |
| shellyplugs | Shelly Plug-S | SHPLG-S |
| shellyem | Shelly EM with integrated Power Meters | SHEM |
| shellyem3 | Shelly 3EM with 3 integrated Power Meter | SHEM-3 |
| shellyrgbw2 | Shelly RGB Controller | SHRGBW2 |
| shellybulb | Shelly Bulb in Color or White Mode | SHBLB-1 |
| shellybulbduo | Shelly Duo (White Mode) | SHBDUO-1 |
| shellyvintage | Shelly Vintage (White Mode) | SHVIN-1 |
| shellyht | Shelly Sensor (temp+humidity) | SHHT-1 |
| shellyflood | Shelly Flood Sensor | SHWT-1 |
| shellysmoke | Shelly Smoke Sensor | |
| shellygas | Shelly Gas Sensor | SHGS-1 |
| shellydw | Shelly Door/Window | SHDW-1 |
| shellydw2 | Shelly Door/Window 2 | SHDW-2 |
| shellybutton1 | Shelly Button 1 | SHBTN-1 |
| shellysense | Shelly Motion and IR Controller | SHSEN-1 |
| shellydevice | A password protected Shelly device or an unknown type | |
## Binding Configuration
The binding has the following configuration options:
| Parameter |Description |Mandatory|Default |
|----------------|------------------------------------------------------------------|---------|------------------------------------------------|
| defaultUserId |Default user id for HTTP authentication when not set in the Thing | no |admin |
| defaultPassword|Default password for HTTP authentication when not set in the Thing| no |admin |
| autoCoIoT |Auto-enable CoIoT events when firmware 1.6+ is enabled. | no |true |
The binding defaults to CoIoT events when firmware 1.6 or newer is detected. CoIoT provides near-realtime updates on device status changes.
This mode also overrules event settings in the thing configuration.
Disabling this feature allows granular control, which event types will be used. This is also required when the Shelly devices are not located on the same IP subnet (e.g. using a VPN).
In this case autoCoIoT should be disabled, CoIoT events will not work, because the underlying CoAP protocol is based on Multicast IP, which usually doesn't passes a VPN or routed network.
## Discovery
In general devices need to be powered to be discovered by the binding.
The binding uses mDNS to discover the Shelly devices.
They periodically announce their presence, which is used by the binding to find them on the local network.
Sometimes you need to run the manual discovery multiple times until you see all your devices.
## Firmware
The binding requires firmware version 1.7.0 or newer to enable all features.
Some of the features are enabled dynamically or are not available depending on device type and firmware release.
The Web UI of the Shelly device displays the current firmware version under Settings:Firmware and shows an update option when a newer version is available.
|Version|Notes |
|-------|--------------------------------------------------------------------------------------------------|
|1.5.7 |Minimum supported version. Older versions work in general, but have impacts to functionality (e.g. no events for battery powered devices). The binding displays a WARNING in the log if the firmware is older.|
|1.6.x |First stable CoIoT implementation. AutoCoIoT is enabled when firmware version >= 1.6 is detected. |
|1.7.x |Add additional status update values, fixes various issues |
|1.8.0 |Brings CoIoT version 2, which fixes a lot issues and gaps of version 1. |
The current firmware version is reported in the Thing Properties.
A dedicated channel (device#updateAvailable) indicates the availability of a newer firmware. Use the device's Web UI or the Shelly App to perform the update.
Once you have updated the device **you should delete and re-discover** the openHAB Thing.
Battery powered devices need to wake up by pressing the button.
This makes sure that the Thing is correctly initialized and all supported channels are created. openHAB will kill the item linkage.
At a minimum you should restart the binding on the openHAB console if you don't want to re-discover the things.
### Password Protected Devices
The Shelly devices can be configured to require authorization through a user id and password.
In this case you need to set these values in the Thing configuration after approving the Inbox entry.
If you have multiple devices protected by the same credentials it's recommended to set the default user id and password in the binding configuration BEFORE running the discovery.
In this case the binding could directly access the device to retrieve the required information using those credentials.
Otherwise a thing of type shellyprotected is created in the Inbox and you could set the credentials while adding the thing. In this case the credentials are persisted as part of the thing configuration.
### Important for battery powered devices
Make sure to wake up battery powered devices, so that they show up on the network.
The device has a push button inside. Open the case, press that button and the LED starts flashing.
Wait a moment and then start the discovery. The device should show up in the Inbox and can be added.
Sometimes you need to run the discovery multiple times.
### Dynamic creation of channels
The Shelly series of devices has many combinations of relays, meters (different versions), sensors etc. For this the binding creates various channels dynamically based on the status information provided by the device at initialization time.
If a channel is missing make sure the thing was discovered correctly and is ONLINE. If a channel is missing delete the thing and re-discover it.
### Thing Status
The binding sets the following thing status depending on the device status:
| Status |Description |
|--------------|------------------------------------------------------------------|
| INITIALIZING | This is the default status while initializing the thing. Once the initialization is triggered the thing switches to Status UNKNOWN. |
| UNKNOWN | Indicates that the status is currently unknown, which must not show a problem. Usually the thing stays in this status when the device is in sleep mode. Once the device is reachable and was initialized the thing switches to status ONLINE.|
| ONLINE | ONLINE indicates that the device can be accessed and is responding properly. Battery powered devices also stay ONLINE when in sleep mode. The binding has an integrated watchdog timer supervising the device, see below. The thing switches to status OFFLINE when some type of communication error occurs. |
| OFFLINE | Communication with the device failed. Check the thing status in the UI and openHAB's log for an indication of the error. Try restarting OH or deleting and re-discovering the thing. You could also post to the community thread if the problem stays. |
`Battery powered devices:`
If the device is in sleep mode and can't be reached by the binding, the thing will change into UNKNOWN state.
Once the device wakes up, the thing will perform initialization and the state will change to ONLINE.
The first time a device is discovered and initialized successfully, the binding will be able to perform auto-initialization when OH is restarted. Waking up the device triggers the event URL and/or CoIoT packet, which is processed by the binding and triggers initialization. Once a device is initialized, it is no longer necessary to manually wake it up after an openHAB restart unless you change the battery. In this case press the button and run the discovery again.
### Device Watchdog
The binding supervises availability of the device once it becomes ONLINE by sending periodic status requests to the device. The watchdog is restarted when the device is responding properly.
Communication errors are handled depending on the device type:
- regular power devices change to OFFLINE, because this status indicates an error
- battery powered devices stay ONLINE, because usually the device is in sleep mode
The binding also monitors that the device is responding at least once within a given time period.
The period is computed depending on the device type and configuration:
- battery powered devices: &lt;sleepPeriod from device config&gt; + 10min, usually 12h+10min=730min
- else, if CoIoT is enabled: 3*&lt;update Period from device settings&gt;+10sec, usually3*15+10=45sec
- else 2*60+10sec = 130sec
Once the timer expires the device switches to OFFFLINE and the bindings starts to re-initialize the device periodically.
You could also create a rule to catch those status changes or device alarms (see rule examples).
## Trouble Shooting
### Network Settings
Shelly devices do only support IPv4.
This implies that the openHAB host system has IPv4 bound to the network interface.
The binding is only able to discover devices on the local subnet.
Add things manually with the given IP if you have a routed network in between or using a VPN connection.
The binding enables CoIoT protocol by default if the device is running firmware 1.6 or newer.
CoIoT is based on CoAP and uses a UDP based signaling using IP Multicast (224.0.1.187, port 5683).
Again if the device is not on the same local IP subnet you need special router/switch configurations to utilized CoAP via IP Multicast.
Otherwise disable the Auto-CoIoT feature in the binding config (not the thing config), disable CoIoT events in the thing configuration and enable sensors events (http callback).
Nevertheless in this setup the binding can communicate the device, but you are loosing the benefits of CoIoT.
Refer to openHAB's general documentation when running openHAB in a docker container. Enabling mDNS discovery has additional setup requirements.
### Re-discover when IP address has changed
Important: The IP address should not be changed after the device is added to openHAB.
This can be achieved by
- assigning a static IP address (recommended for battery powered devices) or
- using DHCP and setup the router to always assign the same IP address to the device
When the IP address changes for a device you need to delete the Thing and then re-discover the device.
In this case channel linkage gets lost and you need to re-link the channels/items.
## Thing Configuration
|Parameter |Description |Mandatory|Default |
|------------------|--------------------------------------------------------------|---------|--------------------------------------------------|
|deviceIp |IP address of the Shelly device | yes |none |
|userId |The user id used for HTTP authentication | no |none |
|password |Password for HTTP authentication* | no |none |
|lowBattery |Threshold for battery level. Set alert when level is below. | no |20 (=20%), only for battery powered devices |
|updateInterval |Interval for the background status check in seconds. | no |1h for battery powered devices, 60s for all others|
|eventsButton |true: register event "trigger when a button is pushed" | no |false |
|eventsPush |true: register event "trigger on short and long push" | no |false |
|eventsSwitch |true: register event "trigger of switching the relay output" | no |true |
|eventsSensorReport|true: register event "posted updated sensor data" | no |true for sensor devices |
|eventsCoIoT |true: Listen for CoIoT/COAP events | no |true for battery devices, false for others |
### General Notes
- channels `input` and `input1`/`input2` get only updated with firmware 1.5.6+.
- channel button: Short push and long push events require firmware version 1.5.6+.
- Use the channel `rollerpos` only if you need the inverted roller position, otherwise use the `control` channel with item type `Number`
- The different devices have different types of power meters, which are mapped in different sets of channels.
Every device has a channel group `device` with the following channels:
|Group |Channel |Type |read-only|Description |
|----------|-------------------|--------|---------|---------------------------------------------------------------------------------|
|device |deviceName |String |yes |Device name as configured in the Shelly App |
| |uptime |Number |yes |Number of seconds since the device was powered up |
| |wifiSignal |Number |yes |WiFi signal strength (4=excellent, 3=good, 2=not string, 1=unreliable, 0=none) |
| |innerTemp |Number |yes |Internal device temperature (when provided by the device) |
| |selfTest |String |yes |Result from device self-test (pending/not_completed/running/completed/unknown) |
| |alarm |Trigger |yes |Self-Test result not_completed/completed/running/pending |
| |accumulatedWatts |Number |yes |Accumulated power in W of the device (including all meters) |
| |accumulatedTotal |Number |yes |Accumulated total power in kw/h of the device (including all meters) |
| |accumulatedReturned|Number |yes |Accumulated returned power in kw/h of the device (including all meters) |
| |heartBeat |DateTime|yes |Timestamp of the last successful device communication |
| |updateAvailable |Switch |yes |ON: A firmware update is available (use Shelly App to perform update) |
| |statusLed |Switch |r/w |ON: Status LED is disabled, OFF: LED enabled |
| |powerLed |Switch |r/w |ON: Power LED is disabled, OFF: LED enabled |
Availability of channels is depending on the device type.
The binding detects many of those channels on-the-fly (when Thing changes to ONLINE state) and adjusts the Thing's channel structure.
The device must be discovered and ONLINE to successfully complete this process.
The accumulated channels are only available for devices with more than 1 meter. accumulatedReturned only for the EM and 3EM.
The LED channels are available for the Plug-S with firmware 1.6x and for various other devices with firmware 1.8 or newer. The binding detects them automatically.
## Events
### Action URLs vs. CoIoT
Depending on the firmware release the Shelly devices supports 2 different mechanims to report sensor updates or events.
1. Action URLs
Usually the binding polls the device to update the status and maps the returned values to the various channels.
In addition the binding can register so-called Action URLs. Those a events triggered by the device to report special events.
You need to disable autoCoIoT in the binding configuration to make specific selections for the Action events.
The following event types could be registered when enabled in the thing configuration:
|Event Type |Description |
|------------------|---------------------------------------------------------------------------------------------------------------|
|eventsButton |This event is triggered when the device is in button mode. The device reports the ON/OFF status oh the button. |
|eventsSwitch |This event reports the status of the relay output. This could change by the button or API calls. |
|eventsPush |The device reports the short/longpush events when in button mode momentary, momentary_on_release or detached. |
|eventsSensorReport|Sensor devices (like H&T) provide sensor updates when this action URL is enabled. |
Important: The binding defaults to CoIoT when firmware 1.6 or newer is detected.
This has significant experience improvements and also prevents interfering with other applications, because the device only supports one set of Action URLs.
2. CoIoT / CoAP
Starting with version 1.6 the devices reports most status values via the CoIoT protocol.
CoIoT provides near-realtime updates and better event support.
Firmware 1.7 adds additional status values, also supported by the binding.
Version 1.8 introduces CoIoT version 2, which fixes various issues with version 1 and provides almost all relevant status updates.
If there is no specific reason you should enable CoIoT. See Network Settings for more information.
Enable the autoCoIoT option in the binding configuration or eventsCoIoT is the thing configuration to activate CoIoT.
### Button events
Various devices signal an event when the physical button is pressed.
This could be a switch connected to the SW input of the relay or the Button 1.
The following trigger types are sent:
|Event Type |Description |
|-------------------|---------------------------------------------------------------------------------------------------------------|
|SHORT_PRESSED |The button was pressed once for a short time |
|DOUBLE_PRESSED |The button was pressed twice with short delay |
|TRIPLE_PRESSED |The button was pressed three times with short delay |
|LONG_PRESSED |The button was pressed for a longer time |
|SHORT_LONG_PRESSED |A short followed by a long button push |
|LONG_SHORT_PRESSED |A long followed by a short button push |
Check the channel definitions for the various devices to see if the device supports those events.
You could use the Shelly App to set the timing for those events.
### Alarms
The binding provides health monitoring functions for the device.
When an alarm condition is detected the channel alarm gets triggered and provides one of the following alarm types:
A new alarm will be triggered on a new condition or every 5 minutes if the condition persists.
### Non-battery powered devices
|Event Type|Description|
|------------|-----------------------------------------------------------------------------------------------------------------|
|RESTARTED |The device has been restarted. This could be an indicator for a firmware problem. |
|WEAK_SIGNAL |An alarm is triggered when RSSI is < -80, which indicates an unstable connection. |
|OVER_TEMP |The device is overheating, check installation and housing. |
|OVER_LOAD |An over load condition has been detected, e.g. from the roller motor. |
|OVER_POWER |Maximum allowed power was exceeded. The relay was turned off. |
|LOAD_ERROR |Device reported a load problem, so far Dimmer only. |
### Sensors
|Event Type|Description|
|------------|-----------------------------------------------------------------------------------------------------------------|
|POWERON |Device was powered on. |
|PERIODIC |Periodic wakeup. |
|BUTTON |Button was pressed, e.g. to wake up the device. |
|SENSOR |Wake-up due to updated sensor data. |
|ALARM |Alarm condition was detected, check status, could be OPENED for the DW, flood alarm, smoke alarm |
|BATTERY |Device reported an update to the battery status. |
|TEMP_UNDER |Below "temperature under" threshold |
|TEMP_OVER |Above "temperature over" threshold |
```
rule "Shelly Alarm"
when
Channel "shelly:shelly1:XXXXXX:device#alarm" triggered
then
logInfo("Shelly", "n alarm condition was detected:" + receivedEvent.toString())
end
```
## Channels
Depending on the device type and firmware release channels might be not available or
### Shelly 1 (thing-type: shelly1)
|Group |Channel |Type |read-only|Description |
|----------|-------------|---------|---------|---------------------------------------------------------------------------------|
|relay |output |Switch |r/w |Controls the relay's output channel (on/off) |
| |input |Switch |yes |ON: Input/Button is powered, see General Notes on Channels |
| |outputName |String |yes |Logical name of this relay output as configured in the Shelly App |
|sensors |temperature1 |Number |yes |Temperature value of external sensor #1 (if connected to temp/hum addon) |
| |temperature2 |Number |yes |Temperature value of external sensor #2 (if connected to temp/hum addon) |
| |temperature3 |Number |yes |Temperature value of external sensor #3 (if connected to temp/hum addon) |
| |humidity |Number |yes |Humidity in percent (if connected to temp/hum addon) |
### Shelly 1PM (thing-type: shelly1pm)
|Group |Channel |Type |read-only|Description |
|----------|-------------|---------|---------|---------------------------------------------------------------------------------|
|relay |output |Switch |r/w |Controls the relay's output channel (on/off) |
| |input |Switch |yes |ON: Input/Button is powered, see General Notes on Channels |
| |button |Trigger |yes |Event trigger with payload SHORT_PRESSED or LONG_PRESSED (FW 1.5.6+) |
| |outputName |String |yes |Logical name of this relay output as configured in the Shelly App |
|meter |currentWatts |Number |yes |Current power consumption in Watts |
| |lastPower1 |Number |yes |Energy consumption in Watts for a round minute, 1 minute ago |
| |lastPower2 |Number |yes |Energy consumption in Watts for a round minute, 2 minutes ago |
| |lastPower3 |Number |yes |Energy consumption in Watts for a round minute, 3 minutes ago |
| |totalKWH |Number |yes |Total energy consumption in Watts since the device powered up (resets on restart)|
| |lastUpdate |DateTime |yes |Timestamp of the last measurement |
|sensors |temperature1 |Number |yes |Temperature value of external sensor #1 (if connected to temp/hum addon) |
| |temperature2 |Number |yes |Temperature value of external sensor #2 (if connected to temp/hum addon) |
| |temperature3 |Number |yes |Temperature value of external sensor #3 (if connected to temp/hum addon) |
| |humidity |Number |yes |Humidity in percent (if connected to temp/hum addon) |
### Shelly EM (thing-type: shellyem)
|Group |Channel |Type |read-only|Description |
|----------|-------------|---------|---------|---------------------------------------------------------------------------------|
|relay |output |Switch |r/w |Controls the relay's output channel (on/off) |
| |input |Switch |yes |ON: Input/Button is powered, see General Notes on Channels |
| |button |Trigger |yes |Event trigger with payload SHORT_PRESSED or LONG_PRESSED (FW 1.5.6+) |
| |outputName |String |yes |Logical name of this relay output as configured in the Shelly App |
|meter1 |currentWatts |Number |yes |Current power consumption in Watts |
| |totalKWH |Number |yes |Total energy consumption in Watts since the device powered up (resets on restart)|
| |returnedKWH |Number |yes |Total returned energy, kw/h |
| |reactiveWatts|Number |yes |Instantaneous reactive power, Watts |
| |voltage |Number |yes |RMS voltage, Volts |
| |lastUpdate |DateTime |yes |Timestamp of the last measurement |
|meter2 |currentWatts |Number |yes |Current power consumption in Watts |
| |totalKWH |Number |yes |Total energy consumption in Watts since the device powered up (resets on restart)|
| |returnedKWH |Number |yes |Total returned energy, kw/h |
| |reactiveWatts|Number |yes |Instantaneous reactive power, Watts |
| |voltage |Number |yes |RMS voltage, Volts |
| |lastUpdate |DateTime |yes |Timestamp of the last measurement |
### Shelly 3EM (thing-type: shellyem3)
Please note: The product is called Shelly 3EM whereas the device propagates the service under shellyem3.
The thing id is derived from the service name, so that's the reason why the thing is named shelly**em3** and not shelly3em.
|Group |Channel |Type |read-only|Description |
|----------|-------------|---------|---------|---------------------------------------------------------------------------------|
|relay |output |Switch |r/w |Controls the relay's output channel (on/off) |
| |input |Switch |yes |ON: Input/Button is powered, see General Notes on Channels |
| |button |Trigger |yes |Event trigger with payload SHORT_PRESSED or LONG_PRESSED (FW 1.5.6+) |
| |outputName |String |yes |Logical name of this relay output as configured in the Shelly App |
|meter1 |currentWatts |Number |yes |Current power consumption in Watts |
| |totalKWH |Number |yes |Total energy consumption in Watts since the device powered up (resets on restart)|
| |returnedKWH |Number |yes |Total returned energy, kw/h |
| |reactiveWatts|Number |yes |Instantaneous reactive power, Watts |
| |voltage |Number |yes |RMS voltage, Volts |
| |current |Number |yes |Current in A |
| |powerFactor |Number |yes |Power Factor |
| |lastUpdate |DateTime |yes |Timestamp of the last measurement |
|meter2 |currentWatts |Number |yes |Current power consumption in Watts |
| |totalKWH |Number |yes |Total energy consumption in Watts since the device powered up (resets on restart)|
| |returnedKWH |Number |yes |Total returned energy, kw/h |
| |reactiveWatts|Number |yes |Instantaneous reactive power, Watts |
| |voltage |Number |yes |RMS voltage, Volts |
| |current |Number |yes |Current in A |
| |powerFactor |Number |yes |Power Factor |
| |lastUpdate |DateTime |yes |Timestamp of the last measurement |
|meter3 |currentWatts |Number |yes |Current power consumption in Watts |
| |totalKWH |Number |yes |Total energy consumption in Watts since the device powered up (resets on restart)|
| |returnedKWH |Number |yes |Total returned energy, kw/h |
| |reactiveWatts|Number |yes |Instantaneous reactive power, Watts |
| |voltage |Number |yes |RMS voltage, Volts |
| |current |Number |yes |Current in A |
| |powerFactor |Number |yes |Power Factor |
| |lastUpdate |DateTime |yes |Timestamp of the last measurement |
### Shelly 2 - relay mode thing-type: shelly2-relay)
|Group |Channel |Type |read-only|Description |
|----------|-------------|---------|---------|---------------------------------------------------------------------------------|
|relay1 |output |Switch |r/w |Relay #1: Controls the relay's output channel (on/off) |
| |input |Switch |yes |ON: Input/Button is powered, see General Notes on Channels |
| |autoOn |Number |r/w |Relay #1: Sets a timer to turn the device ON after every OFF command; in seconds|
| |autoOff |Number |r/w |Relay #1: Sets a timer to turn the device OFF after every ON command; in seconds|
| |timerActive |Switch |yes |Relay #1: ON: An auto-on/off timer is active |
| |button |Trigger |yes |Event trigger with payload SHORT_PRESSED or LONG_PRESSED (FW 1.5.6+) |
| |outputName |String |yes |Logical name of this relay output as configured in the Shelly App |
|relay2 |output |Switch |r/w |Relay #2: Controls the relay's output channel (on/off) |
| |input |Switch |yes |ON: Input/Button is powered, see General Notes on Channels |
| |autoOn |Number |r/w |Relay #2: Sets a timer to turn the device ON after every OFF command; in seconds|
| |autoOff |Number |r/w |Relay #2: Sets a timer to turn the device OFF after every ON command; in seconds|
| |timerActive |Switch |yes |Relay #2: ON: An auto-on/off timer is active |
| |button |Trigger |yes |Event trigger with payload SHORT_PRESSED or LONG_PRESSED (FW 1.5.6+) |
| |outputName |String |yes |Logical name of this relay output as configured in the Shelly App |
|meter |currentWatts |Number |yes |Current power consumption in Watts |
| |lastPower1 |Number |yes |Energy consumption in Watts for a round minute, 1 minute ago |
| |totalKWH |Number |yes |Total energy consumption in Watts since the device powered up (resets on restart)|
| |lastUpdate |DateTime |yes |Timestamp of the last measurement |
### Shelly 2 - roller mode thing-type: shelly2-roller)
|Group |Channel |Type |read-only|Description |
|----------|-------------|---------|---------|--------------------------------------------------------------------------------------|
|roller |control |Rollershutter|r/w |can be open (0%), stop, or close (100%); could also handle ON (open) and OFF (close) |
| |input |Switch |yes |ON: Input/Button is powered, see General Notes on Channels |
| |rollerpos |Number |r/w |Roller position: 100%=open...0%=closed; gets updated when the roller stops, see Notes |
| |state |String |yes |Roller state: open/close/stop |
| |stopReason |String |yes |Last stop reasons: normal, safety_switch or obstacle |
| |event |Trigger |yes |Roller event/trigger with payload ROLLER_OPEN / ROLLER_CLOSE / ROLLER_STOP |
|meter |currentWatts |Number |yes |Current power consumption in Watts |
| |lastPower1 |Number |yes |Accumulated energy consumption in Watts for the full last minute |
| |totalKWH |Number |yes |Total energy consumption in Watts since the device powered up (reset on restart) |
| |lastUpdate |DateTime |yes |Timestamp of the last measurement |
The roller positioning calibration has to be performed using the Shelly App before the position can be set in percent.
### Shelly 2.5 - relay mode (thing-type:shelly25-relay)
The Shelly 2.5 includes 2 meters, one for each channel.
|Group |Channel |Type |read-only|Description |
|----------|-------------|---------|---------|---------------------------------------------------------------------------------|
|relay1 | | | |See group relay1 for Shelly 2 |
|relay2 | | | |See group relay1 for Shelly 2 |
|meter1 | | | |See group meter1 for Shelly 2 |
|meter2 | | | |See group meter1 for Shelly 2 |
### Shelly 2.5 - roller mode (thing-type: shelly25-roller)
The Shelly 2.5 includes 2 meters, one for each channel.
However, it doesn't make sense to differ power consumption for the roller moving up vs. moving down.
For this the binding aggregates the power consumption of both relays and includes the values in "meter1".
|Group |Channel |Type |read-only|Description |
|----------|-------------|---------|---------|-------------------------------------------------------------------------------------|
|roller |control |Rollershutter |r/w |can be open (0%), stop, or close (100%); could also handle ON (open) and OFF (close) |
| |rollerpos |Dimmer |r/w |Roller position: 100%=open...0%=closed; gets updated when the roller stopped |
| |input |Switch |yes |ON: Input/Button is powered, see General Notes on Channels |
| |state |String |yes |Roller state: open/close/stop |
| |stopReason |String |yes |Last stop reasons: normal, safety_switch or obstacle |
| |event |Trigger |yes |Roller event/trigger with payload ROLLER_OPEN / ROLLER_CLOSE / ROLLER_STOP |
|meter | | | |See group meter1 for Shelly 2 |
The roller positioning calibration has to be performed using the Shelly App before the position can be set in percent.
### Shelly4 Pro (thing-type: shelly4pro)
The Shelly 4Pro provides 4 relays and 4 power meters.
|Group |Channel |Type |read-only|Description |
|----------|-------------|---------|---------|---------------------------------------------------------------------------------|
|relay1 | | | |See group relay1 for Shelly 2 |
|relay2 | | | |See group relay1 for Shelly 2 |
|relay3 | | | |See group relay1 for Shelly 2 |
|relay4 | | | |See group relay1 for Shelly 2 |
|meter1 | | | |See group meter1 for Shelly 2 |
|meter2 | | | |See group meter1 for Shelly 2 |
|meter3 | | | |See group meter1 for Shelly 2 |
|meter4 | | | |See group meter1 for Shelly 2 |
### Shelly Plug-S (thing-type: shellyplugs)
|Group |Channel |Type |read-only|Description |
|----------|-------------|---------|---------|---------------------------------------------------------------------------------|
|relay | | | |See group relay1 for Shelly 2 |
|meter | | | |See group meter1 for Shelly 2 |
### Shelly Dimmer (thing-type: shellydimmer)
|Group |Channel |Type |read-only|Description |
|----------|-------------|---------|---------|---------------------------------------------------------------------------------|
|relay |brightness |Dimmer |r/w |Currently selected brightness. |
| |input1 |Switch |yes |State of Input 1 (S1) |
| |input2 |Switch |yes |State of Input 2 (S2) |
| |autoOn |Number |r/w |Sets a timer to turn the device ON after every OFF command; in seconds |
| |autoOff |Number |r/w |Sets a timer to turn the device OFF after every ON command; in seconds |
| |button |Trigger |yes |Event trigger with payload SHORT_PRESSED or LONG_PRESSED (FW 1.5.6+) |
| |outputName |String |yes |Logical name of this relay output as configured in the Shelly App |
|meter |currentWatts |Number |yes |Current power consumption in Watts |
| |lastPower1 |Number |yes |Energy consumption in Watts for a round minute, 1 minute ago |
| |lastPower2 |Number |yes |Energy consumption in Watts for a round minute, 2 minutes ago |
| |lastPower3 |Number |yes |Energy consumption in Watts for a round minute, 3 minutes ago |
| |totalKWH |Number |yes |Total energy consumption in Watts since the device powered up (resets on restart)|
| |lastUpdate |DateTime |yes |Timestamp of the last measurement |
### Shelly Dimmer2 (thing-type: shellydimmer2)
|Group |Channel |Type |read-only|Description |
|----------|-------------|---------|---------|---------------------------------------------------------------------------------|
|relay |brightness |Dimmer |r/w |Currently selected brightness. |
| |input1 |Switch |yes |State of Input 1 (S1) |
| |input2 |Switch |yes |State of Input 2 (S2) |
| |autoOn |Number |r/w |Sets a timer to turn the device ON after every OFF command; in seconds |
| |autoOff |Number |r/w |Sets a timer to turn the device OFF after every ON command; in seconds |
| |button |Trigger |yes |Event trigger with payload SHORT_PRESSED or LONG_PRESSED (FW 1.5.6+) |
| |outputName |String |yes |Logical name of this relay output as configured in the Shelly App |
|meter |currentWatts |Number |yes |Current power consumption in Watts |
| |lastPower1 |Number |yes |Energy consumption in Watts for a round minute, 1 minute ago |
| |lastPower2 |Number |yes |Energy consumption in Watts for a round minute, 2 minutes ago |
| |lastPower3 |Number |yes |Energy consumption in Watts for a round minute, 3 minutes ago |
| |totalKWH |Number |yes |Total energy consumption in Watts since the device powered up (resets on restart)|
| |lastUpdate |DateTime |yes |Timestamp of the last measurement |
The Dimmer should be calibrated using the Shelly App.
### Shelly ix3 (thing-type: shellyix3)
|Group |Channel |Type |read-only|Description |
|----------|-------------|---------|---------|-----------------------------------------------------------------------|
|status |input1 |Switch |yes |State of Input 1 |
| |input2 |Switch |yes |State of Input 2 |
| |input3 |Switch |yes |State of Input 3 |
| |button |Trigger |yes |Event trigger: SHORT_PRESSED, DOUBLE_PRESSED, TRIPLE_PRESSED, LONG_PRESSED, SHORT_LONG_PRESSED or LONG_SHORT_PRESSED |
| |lastEvent |String |yes |S/SS/SSS for 1/2/3x Shortpush or L for Longpush |
| |eventCount |Number |yes |Number of button events |
### Shelly Bulb (thing-type: shellybulb)
|Group |Channel |Type |read-only|Description |
|----------|-------------|---------|---------|-----------------------------------------------------------------------|
|control |power |Switch |r/w |Switch light ON/OFF |
| |mode |Switch |r/w |Color mode: color or white |
| |autoOn |Number |r/w |Sets a timer to turn the device ON after every OFF; in sec |
| |autoOff |Number |r/w |Sets a timer to turn the device OFF after every ON: in sec |
| |timerActive |Switch |yes |ON: An auto-on/off timer is active |
|color | | | |Color settings: only valid in COLOR mode |
| |hsb |HSB |r/w |Represents the color picker (HSBType), control r/g/b, bight not white |
| |full |String |r/w |Set Red / Green / Blue / Yellow / White mode and switch mode |
| | | |r/w |Valid settings: "red", "green", "blue", "yellow", "white" or "r,g,b,w" |
| |red |Dimmer |r/w |Red brightness: 0..100% or 0..255 (control only the red channel) |
| |green |Dimmer |r/w |Green brightness: 0..100% or 0..255 (control only the red channel) |
| |blue |Dimmer |r/w |Blue brightness: 0..100% or 0..255 (control only the red channel) |
| |white |Dimmer |r/w |White brightness: 0..100% or 0..255 (control only the red channel) |
| |gain |Dimmer |r/w |Gain setting: 0..100% or 0..100 |
| |effect |Number |r/w |Puts the light into effect mode: 0..6) |
| | | | | 0=No effect, 1=Meteor Shows, 2=Gradual Change, 3=Breath |
| | | | | 4=Flash, 5=On/Off Gradual, 6=Red/Green Change |
|white | | | |Color settings: only valid in WHITE mode |
| |temperature |Number |r/w |color temperature (K): 0..100% or 3000..6500 |
| |brightness |Dimmer | |Brightness: 0..100% or 0..100 |
#### Shelly Duo (thing-type: shellybulbduo)
|Group |Channel |Type |read-only|Description |
|----------|-------------|---------|---------|-----------------------------------------------------------------------|
|control |autoOn |Number |r/w |Sets a timer to turn the device ON after every OFF; in sec |
| |autoOff |Number |r/w |Sets a timer to turn the device OFF after every ON: in sec |
| |timerActive |Switch |yes |ON: An auto-on/off timer is active |
|white | | | |Color settings: only valid in WHITE mode |
| |temperature |Number |r/w |color temperature (K): 0..100% or 2700..6500 |
| |brightness |Dimmer | |Brightness: 0..100% or 0..100 |
|meter |currentWatts |Number |yes |Current power consumption in Watts |
| |lastPower1 |Number |yes |Energy consumption in Watts for a round minute, 1 minute ago |
| |lastPower2 |Number |yes |Energy consumption in Watts for a round minute, 2 minutes ago |
| |lastPower3 |Number |yes |Energy consumption in Watts for a round minute, 3 minutes ago |
| |totalKWH |Number |yes |Total energy consumption in Watts since the device powered up (resets on restart)|
| |lastUpdate |DateTime |yes |Timestamp of the last measurement |
#### Shelly Vintage (thing-type: shellyvintage)
|Group |Channel |Type |read-only|Description |
|----------|-------------|---------|---------|-----------------------------------------------------------------------|
|control |autoOn |Number |r/w |Sets a timer to turn the device ON after every OFF; in sec |
| |autoOff |Number |r/w |Sets a timer to turn the device OFF after every ON: in sec |
| |timerActive |Switch |yes |ON: An auto-on/off timer is active |
|white | | | |Color settings: only valid in WHITE mode |
| |brightness |Dimmer | |Brightness: 0..100% or 0..100 |
|meter |currentWatts |Number |yes |Current power consumption in Watts |
| |lastPower1 |Number |yes |Energy consumption in Watts for a round minute, 1 minute ago |
| |lastPower2 |Number |yes |Energy consumption in Watts for a round minute, 2 minutes ago |
| |lastPower3 |Number |yes |Energy consumption in Watts for a round minute, 3 minutes ago |
| |totalKWH |Number |yes |Total energy consumption in Watts since the device powered up (resets on restart)|
| |lastUpdate |DateTime |yes |Timestamp of the last measurement |
## Shelly RGBW2 in Color Mode (thing-type: shellyrgbw2-color)
|Group |Channel |Type |read-only|Description |
|----------|-------------|---------|---------|-----------------------------------------------------------------------|
|control |power |Switch |r/w |Switch light ON/OFF |
| |button |Trigger |yes |Event trigger with payload SHORT_PRESSED or LONG_PRESSED (FW 1.5.6+) |
| |autoOn |Number |r/w |Sets a timer to turn the device ON after every OFF command; in seconds|
| |autoOff |Number |r/w |Sets a timer to turn the device OFF after every ON command; in seconds|
| |timerActive |Switch |yes |ON: An auto-on/off timer is active |
|color | | | |Color settings: only valid in COLOR mode |
| |hsb |HSB |r/w |Represents the color picker (HSBType), control r/g/b, bight not white |
| |full |String |r/w |Set Red / Green / Blue / Yellow / White mode and switch mode |
| | | |r/w |Valid settings: "red", "green", "blue", "yellow", "white" or "r,g,b,w" |
| |red |Dimmer |r/w |Red brightness: 0..100% or 0..255 (control only the red channel) |
| |green |Dimmer |r/w |Green brightness: 0..100% or 0..255 (control only the red channel) |
| |blue |Dimmer |r/w |Blue brightness: 0..100% or 0..255 (control only the red channel) |
| |white |Dimmer |r/w |White brightness: 0..100% or 0..255 (control only the red channel) |
| |gain |Dimmer |r/w |Gain setting: 0..100% or 0..100 |
| |effect |Number |r/w |Puts the light into effect mode: 0..3) |
| | | | |0=No effect, 1=Meteor Shower, 2=Gradual Change, 3=Flash |
|meter |currentWatts |Number |yes |Current power consumption in Watts |
### Shelly RGBW2 in White Mode (thing-type: shellyrgbw2-white)
|Group |Channel |Type |read-only|Description |
|----------|-------------|---------|---------|-----------------------------------------------------------------------|
|control |input |Switch |yes |State of Input |
|channel1 |brightness |Dimmer |r/w |Channel 1: Brightness: 0..100, control power state with ON/OFF |
| |button |Trigger |yes |Event trigger with payload SHORT_PRESSED or LONG_PRESSED (FW 1.5.6+) |
| |autoOn |Number |r/w |Sets a timer to turn the device ON after every OFF command; in seconds|
| |autoOff |Number |r/w |Sets a timer to turn the device OFF after every ON command; in seconds|
| |timerActive |Switch |yes |ON: An auto-on/off timer is active |
|channel2 |brightness |Dimmer |r/w |Channel 2: Brightness: 0..100, control power state with ON/OFF |
| |button |Trigger |yes |Event trigger with payload SHORT_PRESSED or LONG_PRESSED (FW 1.5.6+) |
| |autoOn |Number |r/w |Sets a timer to turn the device ON after every OFF command; in seconds|
| |autoOff |Number |r/w |Sets a timer to turn the device OFF after every ON command; in seconds|
| |timerActive |Switch |yes |ON: An auto-on/off timer is active |
|channel3 |brightness |Dimmer |r/w |Channel 3: Brightness: 0..100, control power state with ON/OFF |
| |button |Trigger |yes |Event trigger with payload SHORT_PRESSED or LONG_PRESSED (FW 1.5.6+) |
| |autoOn |Number |r/w |Sets a timer to turn the device ON after every OFF command; in seconds|
| |autoOff |Number |r/w |Sets a timer to turn the device OFF after every ON command; in seconds|
| |timerActive |Switch |yes |ON: An auto-on/off timer is active |
|channel4 |brightness |Dimmer |r/w |Channel 5: Brightness: 0..100, control power state with ON/OFF |
| |button |Trigger |yes |Event trigger with payload SHORT_PRESSED or LONG_PRESSED (FW 1.5.6+) |
| |autoOn |Number |r/w |Sets a timer to turn the device ON after every OFF command; in seconds|
| |autoOff |Number |r/w |Sets a timer to turn the device OFF after every ON command; in seconds|
| |timerActive |Switch |yes |ON: An auto-on/off timer is active |
|meter |currentWatts |Number |yes |Current power consumption in Watts (all channels) |
Please note that the settings of channel group color are only valid in color mode and vice versa for white mode.
The current firmware doesn't support the timestamp report for the meters.
The binding emulates this by using the system time on every update.
In white mode each RGBW2 channel is defined as DimmableLight.
This means that the brightness channel has 2 functions
- Sending ON/OFF (OnOffType) to power on/off the channel
- Sending a Number to set the brightness (percentage 0..100)
Sending brightness 0 will automatically turn off the channel if it's currently on.
Sending brightness > 0 will automatically turn on the channel if it's currently off.
You can define 2 items (1 Switch, 1 Number) mapping to the same channel, see example rules.
### Shelly H&T (thing-type: shellyht)
|Group |Channel |Type |read-only|Description |
|----------|-------------|---------|---------|-----------------------------------------------------------------------|
|sensors |temperature |Number |yes |Temperature, unit is reported by tempUnit |
| |humidity |Number |yes |Relative humidity in % |
| |charger |Number |yes |ON: USB charging cable is |
| |lastUpdate |DateTime |yes |Timestamp of the last update (any sensor value changed) |
|battery |batteryLevel |Number |yes |Battery Level in % |
| |voltage |Number |yes |Voltage of the battery |
| |lowBattery |Switch |yes |Low battery alert (< 20%) |
### Shelly Flood (thing type: shellyflood)
|Group |Channel |Type |read-only|Description |
|----------|-------------|---------|---------|-----------------------------------------------------------------------|
|sensors |temperature |Number |yes |Temperature, unit is reported by tempUnit |
| |flood |Switch |yes |ON: Flooding condition detected, OFF: no flooding |
| |lastUpdate |DateTime |yes |Timestamp of the last update (any sensor value changed) |
|battery |batteryLevel |Number |yes |Battery Level in % |
| |voltage |Number |yes |Voltage of the battery |
| |lowBattery |Switch |yes |Low battery alert (< 20%) |
### Shelly Door/Window (thing type: shellydw)
|Group |Channel |Type |read-only|Description |
|----------|-------------|---------|---------|-----------------------------------------------------------------------|
|sensors |state |Contact |yes |OPEN: Contact is open, CLOSED: Contact is closed |
| |lux |Number |yes |Brightness in Lux |
| |illumination |String |yes |Current illumination: dark/twilight/bright |
| |titl |Number |yes |Tilt in ° (angle), -1 indicates that the sensor is not calibrated |
| |vibration |Switch |yes |ON: Vibration detected |
| |lastUpdate |DateTime |yes |Timestamp of the last update (any sensor value changed) |
| |lastError |String |yes |Last device error. |
|battery |batteryLevel |Number |yes |Battery Level in % |
| |voltage |Number |yes |Voltage of the battery |
| |lowBattery |Switch |yes |Low battery alert (< 20%) |
### Shelly Button 1 (thing type: shellybutton1)
|Group |Channel |Type |read-only|Description |
|----------|-------------|---------|---------|-----------------------------------------------------------------------|
|status |lastEvent |String |yes |S/SS/SSS for 1/2/3x Shortpush or L for Longpush |
| |eventCount |Number |yes |Number of button events |
| |input |Switch |yes |ON: Input/Button is powered, see General Notes on Channels |
| |button |Trigger |yes |Event trigger with payload SHORT_PRESSED, DOUBLE_PRESSED... |
| |lastUpdate |DateTime |yes |Timestamp of the last update (any value changed) |
|battery |batteryLevel |Number |yes |Battery Level in % |
| |voltage |Number |yes |Voltage of the battery |
| |lowBattery |Switch |yes |Low battery alert (< 20%) |
You should calibrate the sensor using the Shelly App to get information on the tilt status.
### Shelly Smoke(thing type: shellysmoke)
|Group |Channel |Type |read-only|Description |
|----------|-------------|---------|---------|-----------------------------------------------------------------------|
|sensors |temperature |Number |yes |Temperature, unit is reported by tempUnit |
| |smoke |Number |yes |ON: Smoke detected |
| |lastUpdate |DateTime |yes |Timestamp of the last update (any sensor value changed) |
| |lastError |String |yes |Last device error. |
|battery |batteryLevel |Number |yes |Battery Level in % |
| |voltage |Number |yes |Voltage of the battery |
| |lowBattery |Switch |yes |Low battery alert (< 20%) |
### Shelly Smoke(thing type: shellygas)
|Group |Channel |Type |read-only|Description |
|----------|-------------|---------|---------|-----------------------------------------------------------------------|
|sensors |ppm |Number |yes |Gas concentration (ppm) |
| |sensorState |String |yes |Sensor state: unknown/warmup/normal/fault |
| |alarmState |String |yes |Alarm state: unknown/none/mild/heavy/test |
| |lastUpdate |DateTime |yes |Timestamp of the last update (any sensor value changed) |
### Shelly Sense (thing-type: shellysense)
|Group |Channel |Type |read-only|Description |
|----------|-------------|---------|---------|-----------------------------------------------------------------------|
|control |key |String |r/w |Send a IR key to the sense. There a 3 different types supported |
| | | | |Stored key: send the key code defined by the App , e.g. 123_1_up |
| | | | |Pronto hex: send a Pronto Code in ex format, e.g. 0000 006C 0022 ... |
| | | | |Pronto base64: in base64 format, will be send 1:1 to the Sense |
| |motionTime |Number |r/w |Define the number of seconds when the Sense should report motion |
| |motionLED |Switch |r/w |Control the motion LED: ON when motion is detected or OFF |
| |charger |Switch |yes |ON: charger connected, OFF: charger not connected. |
|sensors |temperature |Number |yes |Temperature in °C |
| |humidity |Number |yes |Relative humidity in % |
| |lux |Number |yes |Brightness in Lux |
| |motion |Switch |yes |ON: Motion detected, OFF: No motion (check also motionTimer) |
| |lastUpdate |DateTime |yes |Timestamp of the last update (any sensor value changed) |
|battery |batteryLevel |Number |yes |Battery Level in % |
| |batteryAlert |Switch |yes |Low battery alert |
## Full Example
### shelly.things
```
/* Shelly 2.5 Roller */
Thing shelly:shelly25-roller:XXXXX1 "Shelly 25 Roller XXXXX1" @ "Home Theater" [deviceIp="x.x.x.x", userId="", password=""]
Thing shelly:shelly25-roller:XXXXX2 "Shelly 25 Roller XXXXX2" @ "Living Room" [deviceIp="x.x.x.x", userId="admin", password="secret"]
/* Shelly 2.5 Relays */
Thing shelly:shelly25-relay:XXXXX3 "Shelly 25 Relay XXXXX3" @ "Hall Way" [deviceIp="x.x.x.x", userId="", password=""]
Thing shelly:shelly25-relay:XXXXX4 "Shelly 25 Relay XXXXX4" @ "Dining Room" [deviceIp="x.x.x.x", userId="", password=""]
Thing shelly:shelly25-relay:XXXXX5 "Shelly 25 Relay XXXXX5" @ "Bed Room" [deviceIp="x.x.x.x", userId="", password=""]
/* Other *
Thing shelly:shellyht:e01691 "ShellyChimenea" @ "lowerground" [ deviceIp="10.0.55.101", userId="", password="", lowBattery=15 , eventsCoIoT=true ]
Thing shelly:shellyht:e01681 "ShellyDormitorio" @ "upperground" [ deviceIp="10.0.55.102", userId="", password="", lowBattery=15 , eventsCoIoT=true ]
Thing shelly:shellyflood:XXXXXX "ShellyFlood" @ "cellar" [ deviceIp="10.0.0.103", userId="", password="", lowBattery=15, eventsSwitch=true, eventsButton=true, eventsCoIoT=true ]
```
### shelly.items
```
/* Relays */
Switch Shelly_XXXXX3_Relay "Garage Light" {channel="shelly:shelly1:XXXXX3:relay#output"}
Number Shelly_XXXXX3_AutoOnTimer "Garage Light Auto On Timer" {channel="shelly:shelly1:XXXXX3:relay#autoOn"}
Number Shelly_XXXXX3_AutoOffTimer "Garage Light Auto Off Timer" {channel="shelly:shelly1:BA2F18:relay#autoOff"}
Switch Shelly_XXXXX3_Relay "Garage Light" {channel="shelly:shelly1:XXXXX3:relay#output"}
Switch Shelly_XXXXX3_Input "Garage Switch (Input)" {channel="shelly:shelly1:XXXXX3:relay#input"}
/* Sensors */
Number ShellyHT_Dormitorio_Temp "Dormitorio Temperature" <temperature> {channel="shelly:shellyht:e01681:sensors#temperature"}
Number ShellyHT_Dormitorio_Humid "Dormitorio Humidity" <humidity> {channel="shelly:shellyht:e01681:sensors#humidity"}
Number ShellyHT_Dormitorio_Batt "Dormitorio Battery" <battery> {channel="shelly:shellyht:e01681:battery#batteryLevel"}
Number ShellyHT_Chimenea_Temp "Chimenea Temperature" <temperature> {channel="shelly:shellyht:e01691:sensors#temperature"}
Number ShellyHT_Chimenea_Humid "Chimenea Humidity" <humidity> {channel="shelly:shellyht:e01691:sensors#humidity"}
Number ShellyHT_Chimenea_Batt "Chimenea Battery" <battery> {channel="shelly:shellyht:e01691:battery#batteryLevel"}
Number ShellyF_Sotano_Temp "Sotano Temperature" <temperature> {channel="shelly:shellyflood:764fe0:sensors#temperature"}
Number ShellyF_Sotano_Batt "Sotano Battery" <battery> {channel="shelly:shellyflood:764fe0:battery#batteryLevel"}
Switch ShellyF_Sotano_Flood "Sotano Flood Alarm" <alarm> {channel="shelly:shellyflood:764fe0:sensors#flood"}
/* Dimmer */
Switch DimmerSwitch "Light on/off" {channel="shelly:shellydimmer:XXX:relay#brightness"}
Dimmer DimmerBrightness "Garage Light Brightness" {channel="shelly:shellydimmer:XXX:relay#brightness"}
Dimmer DimmerIncDec "Garage Light +/-" {channel="shelly:shellydimmer:XXX:relay#brightness"}
Number Shelly_Power "Bath Room Light Power" {channel="shelly:shelly1:XXXXXX:meter#currentWatts"} /* Power Meter */
```
### shelly.rules
reading colors from color picker:
```
import org.openhab.core.library.types.*
rule "Get input change from garage light"
when
Item Shelly_XXXXX3_Input changed to ON
then
logInfo("Garage", "Light input is ON")
BackDoorLight.sendCommand(ON)
end
rule "Momentary Switch events"
when
Channel "shelly:shellydevice:XXXXXX:relay1#button" triggered SHORT_PRESSED
then
logInfo("Relay", "A short push was detected")
end
rule "Shelly alarms"
when
Channel "shelly:shellydevice:XXXXXX:device#alarm" triggered or
Channel "shelly:shelly25-roller:XXXXXX:device#alarm" triggered
then
if (receivedEvent !== null) { // A (channel) event triggered the rule
eventSource = receivedEvent.getChannel().asString
eventType = receivedEvent.getEvent()
...
}
end
rule "Color changed"
when
Item ShellyColor changed
then
var HSBType hsbValue = ShellyColor.state as HSBType
var int redValue = hsbValue.red.intValue
var int greenValue = hsbValue.green.intValue
var int blueValue = hsbValue.blue.intValue
end
```
### shelly.sitemap
```
sitemap demo label="Home"
{
Frame label="Dimmer" {
Switch item=DimmerSwitch
Slider item=DimmerBrightness
SetPoint item=DimmerIncDec
Number item=ShellyHT_Dormitorio_Temp
Number item=ShellyHT_Chimenea_Humid
Number item=ShellyF_Sotano_Batt
Number item=Shelly_Power
}
}
```

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<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 http://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>3.0.0-SNAPSHOT</version>
</parent>
<artifactId>org.openhab.binding.shelly</artifactId>
<name>openHAB Add-ons :: Bundles :: Shelly Binding</name>
</project>

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<features name="org.openhab.binding.shelly-${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-shelly" description="Shelly Binding" version="${project.version}">
<feature>openhab-runtime-base</feature>
<feature>openhab-transport-coap</feature>
<feature>openhab-transport-mdns</feature>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.shelly/${project.version}</bundle>
</feature>
</features>

View File

@@ -0,0 +1,338 @@
/**
* Copyright (c) 2010-2020 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.shelly.internal;
import java.util.Collections;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.ThingTypeUID;
/**
* The {@link ShellyBindingConstants} class defines common constants, which are
* used across the whole binding.
*
* @author Markus Michels - Initial contribution
*/
@NonNullByDefault
public class ShellyBindingConstants {
public static final String VENDOR = "Shelly";
public static final String BINDING_ID = "shelly";
public static final String SYSTEM_ID = "system";
// Type names
public static final String THING_TYPE_SHELLY1_STR = "shelly1";
public static final String THING_TYPE_SHELLY1PM_STR = "shelly1pm";
public static final String THING_TYPE_SHELLYEM_STR = "shellyem";
public static final String THING_TYPE_SHELLY3EM_STR = "shellyem3"; // bad: misspelled product name, it's 3EM
public static final String THING_TYPE_SHELLY2_PREFIX = "shellyswitch";
public static final String THING_TYPE_SHELLY2_RELAY_STR = "shelly2-relay";
public static final String THING_TYPE_SHELLY2_ROLLER_STR = "shelly2-roller";
public static final String THING_TYPE_SHELLY25_PREFIX = "shellyswitch25";
public static final String THING_TYPE_SHELLY25_RELAY_STR = "shelly25-relay";
public static final String THING_TYPE_SHELLY25_ROLLER_STR = "shelly25-roller";
public static final String THING_TYPE_SHELLY4PRO_STR = "shelly4pro";
public static final String THING_TYPE_SHELLYPLUG_STR = "shellyplug";
public static final String THING_TYPE_SHELLYPLUGS_STR = "shellyplugs";
public static final String THING_TYPE_SHELLYDIMMER_STR = "shellydimmer";
public static final String THING_TYPE_SHELLYDIMMER2_STR = "shellydimmer2";
public static final String THING_TYPE_SHELLYIX3_STR = "shellyix3";
public static final String THING_TYPE_SHELLYBULB_STR = "shellybulb";
public static final String THING_TYPE_SHELLYDUO_STR = "shellybulbduo";
public static final String THING_TYPE_SHELLYVINTAGE_STR = "shellyvintage";
public static final String THING_TYPE_SHELLYRGBW2_PREFIX = "shellyrgbw2";
public static final String THING_TYPE_SHELLYRGBW2_COLOR_STR = "shellyrgbw2-color";
public static final String THING_TYPE_SHELLYRGBW2_WHITE_STR = "shellyrgbw2-white";
public static final String THING_TYPE_SHELLYHT_STR = "shellyht";
public static final String THING_TYPE_SHELLYSMOKE_STR = "shellysmoke";
public static final String THING_TYPE_SHELLYGAS_STR = "shellygas";
public static final String THING_TYPE_SHELLYFLOOD_STR = "shellyflood";
public static final String THING_TYPE_SHELLYDOORWIN_STR = "shellydw";
public static final String THING_TYPE_SHELLYDOORWIN2_STR = "shellydw2";
public static final String THING_TYPE_SHELLYEYE_STR = "shellyseye";
public static final String THING_TYPE_SHELLYSENSE_STR = "shellysense";
public static final String THING_TYPE_SHELLYBUTTON1_STR = "shellybutton1";
public static final String THING_TYPE_SHELLYPROTECTED_STR = "shellydevice";
public static final String THING_TYPE_SHELLYUNKNOWN_STR = "shellyunknown";
// Device Types
public static final String SHELLYDT_1 = "SHSW-1";
public static final String SHELLYDT_1PM = "SHSW-PM";
public static final String SHELLYDT_SHPLG = "SHPLG-1";
public static final String SHELLYDT_SHPLG_S = "SHPLG-S";
public static final String SHELLYDT_SHELLY2 = "SHSW-21";
public static final String SHELLYDT_SHELLY25 = "SHSW-25";
public static final String SHELLYDT_SHPRO = "SHSW-44";
public static final String SHELLYDT_EM = "SHEM";
public static final String SHELLYDT_3EM = "SHEM-3";
public static final String SHELLYDT_HT = "SHHT-1";
public static final String SHELLYDT_DW = "SHDW-1";
public static final String SHELLYDT_DW2 = "SHDW-2";
public static final String SHELLYDT_SENSE = "SHSEN-1";
public static final String SHELLYDT_GAS = "SHGS-1";
public static final String SHELLYDT_DIMMER = "SHDM-1";
public static final String SHELLYDT_DIMMER2 = "SHDM-2";
public static final String SHELLYDT_IX3 = "SHIX3-1";
public static final String SHELLYDT_BULB = "SHBLB-1";
public static final String SHELLYDT_DUO = "SHBDUO-1";
public static final String SHELLYDT_VINTAGE = "SHVIN-1";
public static final String SHELLYDT_RGBW2 = "SHRGBW2";
public static final String SHELLYDT_BUTTON1 = "SHBTN-1";
// List of all Thing Type UIDs
public static final ThingTypeUID THING_TYPE_SHELLY1 = new ThingTypeUID(BINDING_ID, THING_TYPE_SHELLY1_STR);
public static final ThingTypeUID THING_TYPE_SHELLY1PM = new ThingTypeUID(BINDING_ID, THING_TYPE_SHELLY1PM_STR);
public static final ThingTypeUID THING_TYPE_SHELLYEM = new ThingTypeUID(BINDING_ID, THING_TYPE_SHELLYEM_STR);
public static final ThingTypeUID THING_TYPE_SHELLY3EM = new ThingTypeUID(BINDING_ID, THING_TYPE_SHELLY3EM_STR);
public static final ThingTypeUID THING_TYPE_SHELLY2_RELAY = new ThingTypeUID(BINDING_ID,
THING_TYPE_SHELLY2_RELAY_STR);
public static final ThingTypeUID THING_TYPE_SHELLY2_ROLLER = new ThingTypeUID(BINDING_ID,
THING_TYPE_SHELLY2_ROLLER_STR);
public static final ThingTypeUID THING_TYPE_SHELLY25_RELAY = new ThingTypeUID(BINDING_ID,
THING_TYPE_SHELLY25_RELAY_STR);
public static final ThingTypeUID THING_TYPE_SHELLY25_ROLLER = new ThingTypeUID(BINDING_ID,
THING_TYPE_SHELLY25_ROLLER_STR);
public static final ThingTypeUID THING_TYPE_SHELLY4PRO = new ThingTypeUID(BINDING_ID, THING_TYPE_SHELLY4PRO_STR);
public static final ThingTypeUID THING_TYPE_SHELLYPLUG = new ThingTypeUID(BINDING_ID, THING_TYPE_SHELLYPLUG_STR);
public static final ThingTypeUID THING_TYPE_SHELLYPLUGS = new ThingTypeUID(BINDING_ID, THING_TYPE_SHELLYPLUGS_STR);
public static final ThingTypeUID THING_TYPE_SHELLYDIMMER = new ThingTypeUID(BINDING_ID,
THING_TYPE_SHELLYDIMMER_STR);
public static final ThingTypeUID THING_TYPE_SHELLYDIMMER2 = new ThingTypeUID(BINDING_ID,
THING_TYPE_SHELLYDIMMER2_STR);
public static final ThingTypeUID THING_TYPE_SHELLYIX3 = new ThingTypeUID(BINDING_ID, THING_TYPE_SHELLYIX3_STR);
public static final ThingTypeUID THING_TYPE_SHELLYBULB = new ThingTypeUID(BINDING_ID, THING_TYPE_SHELLYBULB_STR);
public static final ThingTypeUID THING_TYPE_SHELLYDUO = new ThingTypeUID(BINDING_ID, THING_TYPE_SHELLYDUO_STR);
public static final ThingTypeUID THING_TYPE_SHELLYVINTAGE = new ThingTypeUID(BINDING_ID,
THING_TYPE_SHELLYVINTAGE_STR);
public static final ThingTypeUID THING_TYPE_SHELLYHT = new ThingTypeUID(BINDING_ID, THING_TYPE_SHELLYHT_STR);
public static final ThingTypeUID THING_TYPE_SHELLYSENSE = new ThingTypeUID(BINDING_ID, THING_TYPE_SHELLYSENSE_STR);
public static final ThingTypeUID THING_TYPE_SHELLYSMOKE = new ThingTypeUID(BINDING_ID, THING_TYPE_SHELLYSMOKE_STR);
public static final ThingTypeUID THING_TYPE_SHELLYGAS = new ThingTypeUID(BINDING_ID, THING_TYPE_SHELLYGAS_STR);
public static final ThingTypeUID THING_TYPE_SHELLYFLOOD = new ThingTypeUID(BINDING_ID, THING_TYPE_SHELLYFLOOD_STR);
public static final ThingTypeUID THING_TYPE_SHELLYDOORWIN = new ThingTypeUID(BINDING_ID,
THING_TYPE_SHELLYDOORWIN_STR);
public static final ThingTypeUID THING_TYPE_SHELLYDOORWIN2 = new ThingTypeUID(BINDING_ID,
THING_TYPE_SHELLYDOORWIN2_STR);
public static final ThingTypeUID THING_TYPE_SHELLYBUTTON1 = new ThingTypeUID(BINDING_ID,
THING_TYPE_SHELLYBUTTON1_STR);
public static final ThingTypeUID THING_TYPE_SHELLYEYE = new ThingTypeUID(BINDING_ID, THING_TYPE_SHELLYEYE_STR);
public static final ThingTypeUID THING_TYPE_SHELLYRGBW2_COLOR = new ThingTypeUID(BINDING_ID,
THING_TYPE_SHELLYRGBW2_COLOR_STR);
public static final ThingTypeUID THING_TYPE_SHELLYRGBW2_WHITE = new ThingTypeUID(BINDING_ID,
THING_TYPE_SHELLYRGBW2_WHITE_STR);
public static final ThingTypeUID THING_TYPE_SHELLYPROTECTED = new ThingTypeUID(BINDING_ID,
THING_TYPE_SHELLYPROTECTED_STR);
public static final ThingTypeUID THING_TYPE_SHELLYUNKNOWN = new ThingTypeUID(BINDING_ID,
THING_TYPE_SHELLYUNKNOWN_STR);
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Collections
.unmodifiableSet(Stream
.of(THING_TYPE_SHELLY1, THING_TYPE_SHELLY1PM, THING_TYPE_SHELLYEM, THING_TYPE_SHELLY3EM,
THING_TYPE_SHELLY2_RELAY, THING_TYPE_SHELLY2_ROLLER, THING_TYPE_SHELLY25_RELAY,
THING_TYPE_SHELLY25_ROLLER, THING_TYPE_SHELLY4PRO, THING_TYPE_SHELLYPLUG,
THING_TYPE_SHELLYPLUGS, THING_TYPE_SHELLYDIMMER, THING_TYPE_SHELLYDIMMER2,
THING_TYPE_SHELLYIX3, THING_TYPE_SHELLYBULB, THING_TYPE_SHELLYDUO, THING_TYPE_SHELLYVINTAGE,
THING_TYPE_SHELLYRGBW2_COLOR, THING_TYPE_SHELLYRGBW2_WHITE, THING_TYPE_SHELLYHT,
THING_TYPE_SHELLYSENSE, THING_TYPE_SHELLYEYE, THING_TYPE_SHELLYSMOKE, THING_TYPE_SHELLYGAS,
THING_TYPE_SHELLYFLOOD, THING_TYPE_SHELLYDOORWIN, THING_TYPE_SHELLYDOORWIN2,
THING_TYPE_SHELLYBUTTON1, THING_TYPE_SHELLYPROTECTED, THING_TYPE_SHELLYUNKNOWN)
.collect(Collectors.toSet()));
// Thing Configuration Properties
public static final String CONFIG_DEVICEIP = "deviceIp";
public static final String CONFIG_HTTP_USERID = "userId";
public static final String CONFIG_HTTP_PASSWORD = "password";
public static final String CONFIG_UPDATE_INTERVAL = "updateInterval";
public static final String PROPERTY_SERVICE_NAME = "serviceName";
public static final String PROPERTY_DEV_NAME = "deviceName";
public static final String PROPERTY_DEV_TYPE = "deviceType";
public static final String PROPERTY_DEV_MODE = "deviceMode";
public static final String PROPERTY_HWREV = "deviceHwRev";
public static final String PROPERTY_HWBATCH = "deviceHwBatch";
public static final String PROPERTY_UPDATE_PERIOD = "devUpdatePeriod";
public static final String PROPERTY_NUM_RELAYS = "numberRelays";
public static final String PROPERTY_NUM_ROLLERS = "numberRollers";
public static final String PROPERTY_NUM_METER = "numberMeters";
public static final String PROPERTY_LAST_ACTIVE = "lastActive";
public static final String PROPERTY_WIFI_NETW = "wifiNetwork";
public static final String PROPERTY_UPDATE_STATUS = "updateStatus";
public static final String PROPERTY_UPDATE_AVAILABLE = "updateAvailable";
public static final String PROPERTY_UPDATE_CURR_VERS = "updateCurrentVersion";
public static final String PROPERTY_UPDATE_NEW_VERS = "updateNewVersion";
public static final String PROPERTY_COAP_DESCR = "coapDeviceDescr";
public static final String PROPERTY_COAP_VERSION = "coapVersion";
public static final String PROPERTY_STATS_TIMEOUTS = "statsTimeoutErrors";
public static final String PROPERTY_STATS_TRECOVERED = "statsTimeoutsRecovered";
public static final String PROPERTY_COIOTAUTO = "coiotAutoEnable";
public static final String PROPERTY_COIOTREFRESH = "coiotAutoRefresh";
// Relay
public static final String CHANNEL_GROUP_RELAY_CONTROL = "relay";
public static final String CHANNEL_OUTPUT_NAME = "outputName";
public static final String CHANNEL_OUTPUT = "output";
public static final String CHANNEL_INPUT = "input";
public static final String CHANNEL_INPUT1 = "input1";
public static final String CHANNEL_INPUT2 = "input2";
public static final String CHANNEL_BRIGHTNESS = "brightness";
public static final String CHANNEL_TIMER_AUTOON = "autoOn";
public static final String CHANNEL_TIMER_AUTOOFF = "autoOff";
public static final String CHANNEL_TIMER_ACTIVE = "timerActive";
// Roller
public static final String CHANNEL_GROUP_ROL_CONTROL = "roller";
public static final String CHANNEL_ROL_CONTROL_CONTROL = "control";
public static final String CHANNEL_ROL_CONTROL_POS = "rollerpos";
public static final String CHANNEL_ROL_CONTROL_TIMER = "timer";
public static final String CHANNEL_ROL_CONTROL_STATE = "state";
public static final String CHANNEL_ROL_CONTROL_STOPR = "stopReason";
// Dimmer
public static final String CHANNEL_GROUP_DIMMER_CONTROL = CHANNEL_GROUP_RELAY_CONTROL;
// Power meter
public static final String CHANNEL_GROUP_METER = "meter";
public static final String CHANNEL_METER_CURRENTWATTS = "currentWatts";
public static final String CHANNEL_METER_LASTMIN = "lastPower";
public static final String CHANNEL_METER_LASTMIN1 = CHANNEL_METER_LASTMIN + "1";
public static final String CHANNEL_METER_TOTALKWH = "totalKWH";
public static final String CHANNEL_EMETER_TOTALRET = "returnedKWH";
public static final String CHANNEL_EMETER_REACTWATTS = "reactiveWatts";
public static final String CHANNEL_EMETER_VOLTAGE = "voltage";
public static final String CHANNEL_EMETER_CURRENT = "current";
public static final String CHANNEL_EMETER_PFACTOR = "powerFactor";
public static final String CHANNEL_GROUP_SENSOR = "sensors";
public static final String CHANNEL_SENSOR_TEMP = "temperature";
public static final String CHANNEL_SENSOR_HUM = "humidity";
public static final String CHANNEL_SENSOR_LUX = "lux";
public static final String CHANNEL_SENSOR_PPM = "ppm";
public static final String CHANNEL_SENSOR_ILLUM = "illumination";
public static final String CHANNEL_SENSOR_VIBRATION = "vibration";
public static final String CHANNEL_SENSOR_TILT = "tilt";
public static final String CHANNEL_SENSOR_FLOOD = "flood";
public static final String CHANNEL_SENSOR_SMOKE = "smoke";
public static final String CHANNEL_SENSOR_CONTACT = "state";
public static final String CHANNEL_SENSOR_VALVE = "valve";
public static final String CHANNEL_SENSOR_SSTATE = "status"; // Shelly Gas
public static final String CHANNEL_SENSOR_ALARM_STATE = "alarmState";
public static final String CHANNEL_SENSOR_MOTION = "motion";
public static final String CHANNEL_SENSOR_ERROR = "lastError";
// External sensors for Shelly1/1PM
public static final String CHANNEL_ESENDOR_TEMP1 = CHANNEL_SENSOR_TEMP + "1";
public static final String CHANNEL_ESENDOR_TEMP2 = CHANNEL_SENSOR_TEMP + "2";
public static final String CHANNEL_ESENDOR_TEMP3 = CHANNEL_SENSOR_TEMP + "3";
public static final String CHANNEL_ESENDOR_HUMIDITY = CHANNEL_SENSOR_HUM;
public static final String CHANNEL_GROUP_SENSE_CONTROL = "control";
public static final String CHANNEL_SENSE_KEY = "key";
public static final String CHANNEL_GROUP_BATTERY = "battery";
public static final String CHANNEL_SENSOR_BAT_LEVEL = "batteryLevel";
public static final String CHANNEL_SENSOR_BAT_LOW = "lowBattery";
public static final String CHANNEL_GROUP_LIGHT_CONTROL = "control";
public static final String CHANNEL_LIGHT_COLOR_MODE = "mode";
public static final String CHANNEL_LIGHT_POWER = "power";
public static final String CHANNEL_LIGHT_DEFSTATE = "defaultState";
public static final String CHANNEL_GROUP_LIGHT_CHANNEL = "channel";
// Bulb/RGBW2 in color mode
public static final String CHANNEL_GROUP_COLOR_CONTROL = "color";
public static final String CHANNEL_COLOR_PICKER = "hsb";
public static final String CHANNEL_COLOR_FULL = "full";
public static final String CHANNEL_COLOR_RED = "red";
public static final String CHANNEL_COLOR_GREEN = "green";
public static final String CHANNEL_COLOR_BLUE = "blue";
public static final String CHANNEL_COLOR_WHITE = "white";
public static final String CHANNEL_COLOR_GAIN = "gain";
public static final String CHANNEL_COLOR_EFFECT = "effect";
// Bulb/RGBW2/Dup in White Mode
public static final String CHANNEL_GROUP_WHITE_CONTROL = "white";
public static final String CHANNEL_COLOR_TEMP = "temperature";
// Device Status
public static final String CHANNEL_GROUP_DEV_STATUS = "device";
public static final String CHANNEL_DEVST_NAME = "deviceName";
public static final String CHANNEL_DEVST_UPTIME = "uptime";
public static final String CHANNEL_DEVST_HEARTBEAT = "heartBeat";
public static final String CHANNEL_DEVST_RSSI = "wifiSignal";
public static final String CHANNEL_DEVST_ITEMP = "internalTemp";
public static final String CHANNEL_DEVST_WAKEUP = "wakeupReason";
public static final String CHANNEL_DEVST_ALARM = "alarm";
public static final String CHANNEL_DEVST_ACCUWATTS = "accumulatedWatts";
public static final String CHANNEL_DEVST_ACCUTOTAL = "accumulatedWTotal";
public static final String CHANNEL_DEVST_ACCURETURNED = "accumulatedReturned";
public static final String CHANNEL_DEVST_CHARGER = "charger";
public static final String CHANNEL_DEVST_UPDATE = "updateAvailable";
public static final String CHANNEL_DEVST_SELFTTEST = "selfTest";
public static final String CHANNEL_LED_STATUS_DISABLE = "statusLed";
public static final String CHANNEL_LED_POWER_DISABLE = "powerLed";
// Button/xi3
public static final String CHANNEL_GROUP_STATUS = "status";
public static final String CHANNEL_STATUS_EVENTTYPE = "lastEvent";
public static final String CHANNEL_STATUS_EVENTCOUNT = "eventCount";
// General
public static final String CHANNEL_LAST_UPDATE = "lastUpdate";
public static final String CHANNEL_EVENT_TRIGGER = "event";
public static final String CHANNEL_BUTTON_TRIGGER = "button";
public static final String SERVICE_TYPE = "_http._tcp.local.";
public static final String SHELLY_API_MIN_FWVERSION = "v1.5.7";// v1.5.7+
public static final String SHELLY_API_MIN_FWCOIOT = "v1.6";// v1.6.0+
public static final String SHELLY_API_FWCOIOT2 = "v1.8";// CoAP 2 with FW 1.8+
// Alarm types/messages
public static final String ALARM_TYPE_NONE = "NONE";
public static final String ALARM_TYPE_RESTARTED = "RESTARTED";
public static final String ALARM_TYPE_OVERTEMP = "OVERTEMP";
public static final String ALARM_TYPE_OVERPOWER = "OVERPOWER";
public static final String ALARM_TYPE_OVERLOAD = "OVERLOAD";
public static final String ALARM_TYPE_LOADERR = "LOAD_ERROR";
public static final String ALARM_TYPE_LOW_BATTERY = "LOW_BATTERY";
// Event types
public static final String EVENT_TYPE_RELAY = "relay";
public static final String EVENT_TYPE_ROLLER = "roller";
public static final String EVENT_TYPE_LIGHT = "light";
public static final String EVENT_TYPE_SENSORDATA = "report";
// URI for the EventServlet
public static final String SHELLY_CALLBACK_URI = "/shelly/event";
public static final int DIM_STEPSIZE = 5;
// Formatting: Number of scaling digits
public static final int DIGITS_NONE = 0;
public static final int DIGITS_WATT = 1;
public static final int DIGITS_KWH = 3;
public static final int DIGITS_VOLT = 1;
public static final int DIGITS_TEMP = 1;
public static final int DIGITS_LUX = 1;
public static final int DIGITS_PERCENT = 1;
public static final int SHELLY_API_TIMEOUT_MS = 5000;
public static final int UPDATE_STATUS_INTERVAL_SECONDS = 3; // check for updates every x sec
public static final int UPDATE_SKIP_COUNT = 20; // update every x triggers or when a key was pressed
public static final int UPDATE_MIN_DELAY = 15;// update every x triggers or when a key was pressed
public static final int UPDATE_SETTINGS_INTERVAL_SECONDS = 60; // check for updates every x sec
public static final int HEALTH_CHECK_INTERVAL_SEC = 300; // Health check interval, 5min
}

View File

@@ -0,0 +1,172 @@
/**
* Copyright (c) 2010-2020 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.shelly.internal;
import static org.openhab.binding.shelly.internal.ShellyBindingConstants.*;
import java.util.Map;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.util.ConcurrentHashSet;
import org.openhab.binding.shelly.internal.coap.ShellyCoapServer;
import org.openhab.binding.shelly.internal.config.ShellyBindingConfiguration;
import org.openhab.binding.shelly.internal.handler.ShellyBaseHandler;
import org.openhab.binding.shelly.internal.handler.ShellyLightHandler;
import org.openhab.binding.shelly.internal.handler.ShellyProtectedHandler;
import org.openhab.binding.shelly.internal.handler.ShellyRelayHandler;
import org.openhab.binding.shelly.internal.util.ShellyTranslationProvider;
import org.openhab.binding.shelly.internal.util.ShellyUtils;
import org.openhab.core.i18n.LocaleProvider;
import org.openhab.core.i18n.TranslationProvider;
import org.openhab.core.io.net.http.HttpClientFactory;
import org.openhab.core.net.HttpServiceUtil;
import org.openhab.core.net.NetworkAddressService;
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.ComponentContext;
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;
import io.reactivex.annotations.NonNull;
/**
* The {@link ShellyHandlerFactory} is responsible for creating things and thing handlers.
*
* @author Markus Michels - Initial contribution
*/
@NonNullByDefault
@Component(service = { ThingHandlerFactory.class, ShellyHandlerFactory.class }, configurationPid = "binding.shelly")
public class ShellyHandlerFactory extends BaseThingHandlerFactory {
private final Logger logger = LoggerFactory.getLogger(ShellyHandlerFactory.class);
private final HttpClient httpClient;
private final ShellyTranslationProvider messages;
private final ShellyCoapServer coapServer;
private final Set<ShellyBaseHandler> deviceListeners = new ConcurrentHashSet<>();
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = ShellyBindingConstants.SUPPORTED_THING_TYPES_UIDS;
private ShellyBindingConfiguration bindingConfig = new ShellyBindingConfiguration();
private String localIP = "";
private int httpPort = -1;
/**
* Activate the bundle: save properties
*
* @param componentContext
* @param configProperties set of properties from cfg (use same names as in
* thing config)
*/
@Activate
public ShellyHandlerFactory(@Reference NetworkAddressService networkAddressService,
@Reference LocaleProvider localeProvider, @Reference TranslationProvider i18nProvider,
@Reference HttpClientFactory httpClientFactory, ComponentContext componentContext,
Map<String, Object> configProperties) {
logger.debug("Activate Shelly HandlerFactory");
super.activate(componentContext);
messages = new ShellyTranslationProvider(bundleContext.getBundle(), i18nProvider, localeProvider);
localIP = ShellyUtils.getString(networkAddressService.getPrimaryIpv4HostAddress());
if (localIP.isEmpty()) {
logger.warn("{}", messages.get("message.init.noipaddress"));
}
this.httpClient = httpClientFactory.getCommonHttpClient();
httpPort = HttpServiceUtil.getHttpServicePort(componentContext.getBundleContext());
if (httpPort == -1) {
httpPort = 8080;
}
logger.debug("Using OH HTTP port {}", httpPort);
this.coapServer = new ShellyCoapServer();
// Save bindingConfig & pass it to all registered listeners
bindingConfig.updateFromProperties(configProperties);
}
@Override
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
}
@Override
protected @Nullable ThingHandler createHandler(Thing thing) {
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
String thingType = thingTypeUID.getId();
ShellyBaseHandler handler = null;
if (thingType.equals(THING_TYPE_SHELLYPROTECTED_STR)) {
logger.debug("{}: Create new thing of type {} using ShellyProtectedHandler", thing.getLabel(),
thingTypeUID.toString());
handler = new ShellyProtectedHandler(thing, messages, bindingConfig, coapServer, localIP, httpPort,
httpClient);
} else if (thingType.equals(THING_TYPE_SHELLYBULB.getId()) || thingType.equals(THING_TYPE_SHELLYDUO.getId())
|| thingType.equals(THING_TYPE_SHELLYRGBW2_COLOR.getId())
|| thingType.equals(THING_TYPE_SHELLYRGBW2_WHITE.getId())) {
logger.debug("{}: Create new thing of type {} using ShellyLightHandler", thing.getLabel(),
thingTypeUID.toString());
handler = new ShellyLightHandler(thing, messages, bindingConfig, coapServer, localIP, httpPort, httpClient);
} else if (SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID)) {
logger.debug("{}: Create new thing of type {} using ShellyRelayHandler", thing.getLabel(),
thingTypeUID.toString());
handler = new ShellyRelayHandler(thing, messages, bindingConfig, coapServer, localIP, httpPort, httpClient);
}
if (handler != null) {
deviceListeners.add(handler);
return handler;
}
logger.debug("Unable to create Thing Handler instance!");
return null;
}
/**
* Remove handler of things.
*/
@Override
protected synchronized void removeHandler(@NonNull ThingHandler thingHandler) {
if (thingHandler instanceof ShellyBaseHandler) {
deviceListeners.remove(thingHandler);
}
}
/**
* Dispatch event to registered devices.
*
* @param deviceName
* @param componentIndex Index of component, e.g. 2 for relay2
* @param eventType Type of event, e.g. light
* @param parameters Input parameters from URL, e.g. on sensor reports
*/
public void onEvent(String ipAddress, String deviceName, String componentIndex, String eventType,
Map<String, String> parameters) {
logger.trace("{}: Dispatch event to thing handler", deviceName);
for (ShellyBaseHandler listener : deviceListeners) {
if (listener.onEvent(ipAddress, deviceName, componentIndex, eventType, parameters)) {
// event processed
return;
}
}
}
public ShellyBindingConfiguration getBindingConfig() {
return bindingConfig;
}
}

View File

@@ -0,0 +1,133 @@
/**
* Copyright (c) 2010-2020 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.shelly.internal.api;
import java.net.MalformedURLException;
import java.net.UnknownHostException;
import java.text.MessageFormat;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeoutException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import com.google.gson.JsonSyntaxException;
/**
* The {@link CarNetException} implements an extension to the standard Exception class. This allows to keep also the
* result of the last API call (e.g. including the http status code in the message).
*
* @author Markus Michels - Initial contribution
*/
@NonNullByDefault
public class ShellyApiException extends Exception {
private static final long serialVersionUID = -5809459454769761821L;
private ShellyApiResult apiResult = new ShellyApiResult();
private static String NONE = "none";
public ShellyApiException(Exception exception) {
super(exception);
}
public ShellyApiException(String message) {
super(message);
}
public ShellyApiException(ShellyApiResult res) {
super(NONE);
apiResult = res;
}
public ShellyApiException(String message, Exception exception) {
super(message, exception);
}
public ShellyApiException(ShellyApiResult result, Exception exception) {
super(exception);
apiResult = result;
}
@Override
public String getMessage() {
return isEmpty() ? "" : nonNullString(super.getMessage());
}
@Override
public String toString() {
String message = nonNullString(super.getMessage());
String cause = getCauseClass().toString();
if (!isEmpty()) {
if (isUnknownHost()) {
String[] string = message.split(": "); // java.net.UnknownHostException: api.rach.io
message = MessageFormat.format("Unable to connect to {0} (Unknown host / Network down / Low signal)",
string[1]);
} else if (isMalformedURL()) {
message = MessageFormat.format("Invalid URL: {0}", apiResult.getUrl());
} else if (isTimeout()) {
message = MessageFormat.format("Device unreachable or API Timeout ({0})", apiResult.getUrl());
} else {
message = MessageFormat.format("{0} ({1})", message, cause);
}
} else {
message = apiResult.toString();
}
return message;
}
public boolean isApiException() {
return getCauseClass() == ShellyApiException.class;
}
public boolean isTimeout() {
Class<?> extype = !isEmpty() ? getCauseClass() : null;
return (extype != null) && ((extype == TimeoutException.class) || (extype == ExecutionException.class)
|| (extype == InterruptedException.class) || getMessage().toLowerCase().contains("timeout"));
}
public boolean isHttpAccessUnauthorized() {
return apiResult.isHttpAccessUnauthorized();
}
public boolean isUnknownHost() {
return getCauseClass() == MalformedURLException.class;
}
public boolean isMalformedURL() {
return getCauseClass() == UnknownHostException.class;
}
public boolean isJSONException() {
return getCauseClass() == JsonSyntaxException.class;
}
public ShellyApiResult getApiResult() {
return apiResult;
}
private boolean isEmpty() {
return nonNullString(super.getMessage()).equals(NONE);
}
private static String nonNullString(@Nullable String s) {
return s != null ? s : "";
}
private Class<?> getCauseClass() {
Throwable cause = getCause();
if (getCause() != null) {
return cause.getClass();
}
return ShellyApiException.class;
}
}

View File

@@ -0,0 +1,96 @@
/**
* Copyright (c) 2010-2020 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.shelly.internal.api;
import static org.eclipse.jetty.http.HttpStatus.*;
import static org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.*;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.client.api.Request;
/**
* The {@link ShellyApiResult} wraps up the API result and provides some more information like url, http code, received
* response etc.
*
* @author Markus Michels - Initial contribution
*/
@NonNullByDefault
public class ShellyApiResult {
public String url = "";
public String method = "";
public String response = "";
public int httpCode = -1;
public String httpReason = "";
public ShellyApiResult() {
}
public ShellyApiResult(String method, String url) {
this.method = method;
this.url = url;
}
public ShellyApiResult(ContentResponse contentResponse) {
fillFromResponse(contentResponse);
}
public String getUrl() {
return !url.isEmpty() ? method + " " + url : "";
}
public String getHttpResponse() {
return response;
}
@Override
public String toString() {
return getUrl() + " > " + getHttpResponse();
}
public boolean isHttpOk() {
return httpCode == OK_200;
}
public boolean isHttpAccessUnauthorized() {
return (httpCode == UNAUTHORIZED_401 || response.contains(SHELLY_APIERR_UNAUTHORIZED));
}
public boolean isHttpTimeout() {
return httpCode == -1 || response.toUpperCase().contains(SHELLY_APIERR_TIMEOUT.toLowerCase());
}
public boolean isHttpServerError() {
return httpCode == INTERNAL_SERVER_ERROR_500;
}
public boolean isNotCalibrtated() {
return getHttpResponse().contains(SHELLY_APIERR_NOT_CALIBRATED);
}
private void fillFromResponse(@Nullable ContentResponse contentResponse) {
if (contentResponse != null) {
String r = contentResponse.getContentAsString();
response = r != null ? r : "";
httpCode = contentResponse.getStatus();
httpReason = contentResponse.getReason();
Request request = contentResponse.getRequest();
if (request != null) {
url = request.getURI().toString();
method = request.getMethod();
}
}
}
}

View File

@@ -0,0 +1,280 @@
/**
* Copyright (c) 2010-2020 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.shelly.internal.api;
import static org.openhab.binding.shelly.internal.ShellyBindingConstants.*;
import static org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.*;
import static org.openhab.binding.shelly.internal.util.ShellyUtils.*;
import java.util.HashMap;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsDimmer;
import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsGlobal;
import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsInput;
import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsRelay;
import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsStatus;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
import com.google.gson.JsonSyntaxException;
/**
* The {@link ShellyDeviceProfile} creates a device profile based on the settings returned from the API's /settings
* call. This is used to be more dynamic in controlling the device, but also to overcome some issues in the API (e.g.
* RGBW2 returns "no meter" even it has one)
*
* @author Markus Michels - Initial contribution
*/
@NonNullByDefault
public class ShellyDeviceProfile {
private final Logger logger = LoggerFactory.getLogger(ShellyDeviceProfile.class);
public boolean initialized = false; // true when initialized
public String thingName = "";
public String deviceType = "";
public String settingsJson = "";
public ShellySettingsGlobal settings = new ShellySettingsGlobal();
public ShellySettingsStatus status = new ShellySettingsStatus();
public String hostname = "";
public String mode = "";
public boolean discoverable = true;
public String hwRev = "";
public String hwBatchId = "";
public String mac = "";
public String fwId = "";
public String fwVersion = "";
public String fwDate = "";
public boolean hasRelays = false; // true if it has at least 1 power meter
public int numRelays = 0; // number of relays/outputs
public int numRollers = 0; // number of Rollers, usually 1
public boolean isRoller = false; // true for Shelly2 in roller mode
public boolean isDimmer = false; // true for a Shelly Dimmer (SHDM-1)
public int numMeters = 0;
public boolean isEMeter = false; // true for ShellyEM/3EM
public boolean isLight = false; // true if it is a Shelly Bulb/RGBW2
public boolean isBulb = false; // true only if it is a Bulb
public boolean isDuo = false; // true only if it is a Duo
public boolean isRGBW2 = false; // true only if it a a RGBW2
public boolean inColor = false; // true if bulb/rgbw2 is in color mode
public boolean isSensor = false; // true for HT & Smoke
public boolean hasBattery = false; // true if battery device
public boolean isSense = false; // true if thing is a Shelly Sense
public boolean isHT = false; // true for H&T
public boolean isDW = false; // true for Door Window sensor
public boolean isButton = false; // true for a Shelly Button 1
public boolean isIX3 = false; // true for a Shelly IX
public int minTemp = 0; // Bulb/Duo: Min Light Temp
public int maxTemp = 0; // Bulb/Duo: Max Light Temp
public int updatePeriod = 2 * UPDATE_SETTINGS_INTERVAL_SECONDS + 10;
public Map<String, String> irCodes = new HashMap<>(); // Sense: list of stored IR codes
public ShellyDeviceProfile() {
}
public ShellyDeviceProfile initialize(String thingType, String json) throws ShellyApiException {
Gson gson = new Gson();
initialized = false;
try {
initFromThingType(thingType);
settingsJson = json;
settings = gson.fromJson(json, ShellySettingsGlobal.class);
} catch (IllegalArgumentException | JsonSyntaxException e) {
throw new ShellyApiException(
thingName + ": Unable to transform settings JSON " + e.toString() + ", json='" + json + "'", e);
}
// General settings
deviceType = getString(settings.device.type);
mac = getString(settings.device.mac);
hostname = settings.device.hostname != null && !settings.device.hostname.isEmpty()
? settings.device.hostname.toLowerCase()
: "shelly-" + mac.toUpperCase().substring(6, 11);
mode = !getString(settings.mode).isEmpty() ? getString(settings.mode).toLowerCase() : "";
hwRev = settings.hwinfo != null ? getString(settings.hwinfo.hwRevision) : "";
hwBatchId = settings.hwinfo != null ? getString(settings.hwinfo.batchId.toString()) : "";
fwDate = substringBefore(settings.fw, "/");
fwVersion = substringBetween(settings.fw, "/", "@");
fwId = substringAfter(settings.fw, "@");
discoverable = (settings.discoverable == null) || settings.discoverable;
inColor = isLight && mode.equalsIgnoreCase(SHELLY_MODE_COLOR);
numRelays = !isLight ? getInteger(settings.device.numOutputs) : 0;
if ((numRelays > 0) && (settings.relays == null)) {
numRelays = 0;
}
isDimmer = deviceType.equalsIgnoreCase(SHELLYDT_DIMMER) || deviceType.equalsIgnoreCase(SHELLYDT_DIMMER2);
hasRelays = (numRelays > 0) || isDimmer;
numRollers = getInteger(settings.device.numRollers);
isEMeter = settings.emeters != null;
numMeters = !isEMeter ? getInteger(settings.device.numMeters) : getInteger(settings.device.numEMeters);
if ((numMeters == 0) && isLight) {
// RGBW2 doesn't report, but has one
numMeters = inColor ? 1 : getInteger(settings.device.numOutputs);
}
isRoller = mode.equalsIgnoreCase(SHELLY_MODE_ROLLER);
if (settings.sleepMode != null) {
// Sensor, usally 12h
updatePeriod = getString(settings.sleepMode.unit).equalsIgnoreCase("m") ? settings.sleepMode.period * 60 // minutes
: settings.sleepMode.period * 3600; // hours
updatePeriod += 600; // give 10min extra
} else if ((settings.coiot != null) && (settings.coiot.updatePeriod != null)) {
// Derive from CoAP update interval, usually 2*15+5s=50sec -> 70sec
updatePeriod = Math.max(UPDATE_SETTINGS_INTERVAL_SECONDS, 3 * getInteger(settings.coiot.updatePeriod)) + 10;
} else {
updatePeriod = 2 * UPDATE_SETTINGS_INTERVAL_SECONDS + 10;
}
initialized = true;
return this;
}
public boolean containsEventUrl(String eventType) {
return containsEventUrl(settingsJson, eventType);
}
public boolean containsEventUrl(String json, String eventType) {
String settings = json.toLowerCase();
return settings.contains((eventType + SHELLY_EVENTURL_SUFFIX).toLowerCase());
}
public boolean isInitialized() {
return initialized;
}
public void initFromThingType(String name) {
String thingType = (name.contains("-") ? substringBefore(name, "-") : name).toLowerCase().trim();
if (thingType.isEmpty()) {
return;
}
isBulb = thingType.equals(THING_TYPE_SHELLYBULB_STR);
isDuo = thingType.equals(THING_TYPE_SHELLYDUO_STR) || thingType.equals(THING_TYPE_SHELLYVINTAGE_STR);
isRGBW2 = thingType.startsWith(THING_TYPE_SHELLYRGBW2_PREFIX);
isLight = isBulb || isDuo || isRGBW2;
if (isLight) {
minTemp = isBulb ? MIN_COLOR_TEMP_BULB : MIN_COLOR_TEMP_DUO;
maxTemp = isBulb ? MAX_COLOR_TEMP_BULB : MAX_COLOR_TEMP_DUO;
}
boolean isFlood = thingType.equals(THING_TYPE_SHELLYFLOOD_STR);
boolean isSmoke = thingType.equals(THING_TYPE_SHELLYSMOKE_STR);
boolean isGas = thingType.equals(THING_TYPE_SHELLYGAS_STR);
isHT = thingType.equals(THING_TYPE_SHELLYHT_STR);
isDW = thingType.equals(THING_TYPE_SHELLYDOORWIN_STR) || thingType.equals(THING_TYPE_SHELLYDOORWIN2_STR);
isSense = thingType.equals(THING_TYPE_SHELLYSENSE_STR);
isIX3 = thingType.equals(THING_TYPE_SHELLYIX3_STR);
isButton = thingType.equals(THING_TYPE_SHELLYBUTTON1_STR);
isSensor = isHT || isFlood || isDW || isSmoke || isGas || isButton || isSense;
hasBattery = isHT || isFlood || isDW || isSmoke || isButton; // we assume that Sense is connected to // the
// charger
}
public String getControlGroup(int i) {
if (i < 0) {
logger.debug("{}: Invalid index {} for getControlGroup()", thingName, i);
return "";
}
int idx = i + 1;
if (isDimmer) {
return CHANNEL_GROUP_DIMMER_CONTROL;
} else if (isRoller) {
return numRollers == 1 ? CHANNEL_GROUP_ROL_CONTROL : CHANNEL_GROUP_ROL_CONTROL + idx;
} else if (hasRelays) {
return numRelays == 1 ? CHANNEL_GROUP_RELAY_CONTROL : CHANNEL_GROUP_RELAY_CONTROL + idx;
} else if (isLight) {
return numRelays == 1 ? CHANNEL_GROUP_LIGHT_CONTROL : CHANNEL_GROUP_LIGHT_CONTROL + idx;
} else if (isButton) {
return CHANNEL_GROUP_STATUS;
} else if (isSensor) {
return CHANNEL_GROUP_SENSOR;
}
// e.g. ix3
return numRelays == 1 ? CHANNEL_GROUP_STATUS : CHANNEL_GROUP_STATUS + idx;
}
public String getInputGroup(int i) {
int idx = i + 1; // group names are 1-based
if (isRGBW2) {
return CHANNEL_GROUP_LIGHT_CONTROL;
} else if (isIX3) {
return CHANNEL_GROUP_STATUS + idx;
} else if (isButton) {
return CHANNEL_GROUP_STATUS;
} else if (isRoller) {
return numRelays <= 2 ? CHANNEL_GROUP_ROL_CONTROL : CHANNEL_GROUP_ROL_CONTROL + idx;
} else {
// Device has 1 input per relay: 0=off, 1+2 depend on switch mode
return numRelays <= 1 ? CHANNEL_GROUP_RELAY_CONTROL : CHANNEL_GROUP_RELAY_CONTROL + idx;
}
}
public String getInputChannel(int i) {
int idx = i + 1; // channel names are 1-based
if (isRGBW2 || isIX3) {
return CHANNEL_INPUT; // RGBW2 has only 1 channel
} else if (hasRelays) {
return CHANNEL_INPUT + idx;
}
return CHANNEL_INPUT;
}
public boolean inButtonMode(int idx) {
if (idx < 0) {
logger.debug("{}: Invalid index {} for inButtonMode()", thingName, idx);
return false;
}
String btnType = "";
if (isButton) {
return true;
} else if (isIX3) {
if ((settings.inputs != null) && (idx >= 0) && (idx < settings.inputs.size())) {
ShellySettingsInput input = settings.inputs.get(idx);
btnType = input.btnType;
}
} else if (isDimmer) {
if ((settings.dimmers != null) && (idx >= 0) && (idx < settings.dimmers.size())) {
ShellySettingsDimmer dimmer = settings.dimmers.get(idx);
btnType = dimmer.btnType;
}
} else if ((settings.relays != null) && (idx >= 0) && (idx < settings.relays.size())) {
ShellySettingsRelay relay = settings.relays.get(idx);
btnType = relay.btnType;
}
if (btnType.equals(SHELLY_BTNT_MOMENTARY) || btnType.equals(SHELLY_BTNT_MOM_ON_RELEASE)
|| btnType.equals(SHELLY_BTNT_DETACHED) || btnType.equals(SHELLY_BTNT_ONE_BUTTON)) {
return true;
}
return false;
}
}

View File

@@ -0,0 +1,132 @@
/**
* Copyright (c) 2010-2020 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.shelly.internal.api;
import static org.openhab.binding.shelly.internal.ShellyBindingConstants.*;
import static org.openhab.binding.shelly.internal.util.ShellyUtils.*;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import java.util.TreeMap;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.shelly.internal.ShellyHandlerFactory;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.ConfigurationPolicy;
import org.osgi.service.component.annotations.Deactivate;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.http.HttpService;
import org.osgi.service.http.NamespaceException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* {@link ShellyEventServlet} implements a servlet. which is called by the Shelly device to signnal events (button,
* relay output, sensor data). The binding automatically sets those vent urls on startup (when not disabled in the thing
* config).
*
* @author Markus Michels - Initial contribution
*/
@NonNullByDefault
@Component(service = HttpServlet.class, configurationPolicy = ConfigurationPolicy.OPTIONAL, immediate = true)
public class ShellyEventServlet extends HttpServlet {
private static final long serialVersionUID = 549582869577534569L;
private final Logger logger = LoggerFactory.getLogger(ShellyEventServlet.class);
private final HttpService httpService;
private final ShellyHandlerFactory handlerFactory;
@Activate
public ShellyEventServlet(@Reference HttpService httpService, @Reference ShellyHandlerFactory handlerFactory,
Map<String, Object> config) {
this.httpService = httpService;
this.handlerFactory = handlerFactory;
try {
httpService.registerServlet(SHELLY_CALLBACK_URI, this, null, httpService.createDefaultHttpContext());
logger.debug("ShellyEventServlet started at '{}'", SHELLY_CALLBACK_URI);
} catch (NamespaceException | ServletException | IllegalArgumentException e) {
logger.warn("Could not start CallbackServlet", e);
}
}
@Deactivate
protected void deactivate() {
httpService.unregister(SHELLY_CALLBACK_URI);
logger.debug("ShellyEventServlet stopped");
}
@Override
protected void service(@Nullable HttpServletRequest request, @Nullable HttpServletResponse resp)
throws ServletException, IOException, IllegalArgumentException {
String path = "";
String deviceName = "";
String index = "";
String type = "";
if ((request == null) || (resp == null)) {
logger.debug("request or resp must not be null!");
return;
}
try {
path = request.getRequestURI().toLowerCase();
String ipAddress = request.getHeader("HTTP_X_FORWARDED_FOR");
if (ipAddress == null) {
ipAddress = request.getRemoteAddr();
}
Map<String, String[]> parameters = request.getParameterMap();
logger.debug("CallbackServlet: {} Request from {}:{}{}?{}", request.getProtocol(), ipAddress,
request.getRemotePort(), path, parameters.toString());
if (!path.toLowerCase().startsWith(SHELLY_CALLBACK_URI) || !path.contains("/event/shelly")) {
logger.warn("CallbackServlet received unknown request: path = {}", path);
return;
}
// URL looks like
// <ip address>:<remote port>/shelly/event/shellyrelay-XXXXXX/relay/n?xxxxx or
// <ip address>:<remote port>/shelly/event/shellyrelay-XXXXXX/roller/n?xxxxx or
// <ip address>:<remote port>/shelly/event/shellyht-XXXXXX/sensordata?hum=53,temp=26.50
deviceName = substringBetween(path, "/event/", "/").toLowerCase();
if (path.contains("/" + EVENT_TYPE_RELAY + "/") || path.contains("/" + EVENT_TYPE_ROLLER + "/")
|| path.contains("/" + EVENT_TYPE_LIGHT + "/")) {
index = substringAfterLast(path, "/").toLowerCase();
type = substringBetween(path, deviceName + "/", "/" + index);
} else {
index = "";
type = substringAfterLast(path, "/").toLowerCase();
}
logger.trace("{}: Process event of type type={}, index={}", deviceName, type, index);
Map<String, String> parms = new TreeMap<>();
for (Map.Entry<String, String[]> p : parameters.entrySet()) {
parms.put(p.getKey(), p.getValue()[0]);
}
handlerFactory.onEvent(ipAddress, deviceName, index, type, parms);
} catch (IllegalArgumentException e) {
logger.debug("{}: Exception processing callback: path={}; index={}, type={}, parameters={}", deviceName,
path, index, type, request.getParameterMap().toString());
} finally {
resp.setCharacterEncoding(StandardCharsets.UTF_8.toString());
resp.getWriter().write("");
}
}
}

View File

@@ -0,0 +1,575 @@
/**
* Copyright (c) 2010-2020 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.shelly.internal.api;
import static org.openhab.binding.shelly.internal.ShellyBindingConstants.*;
import static org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.*;
import static org.openhab.binding.shelly.internal.util.ShellyUtils.*;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.HttpStatus;
import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellyControlRoller;
import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySendKeyList;
import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySenseKeyCode;
import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsDevice;
import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsLight;
import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsStatus;
import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellyShortLightStatus;
import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellyStatusLight;
import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellyStatusRelay;
import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellyStatusSensor;
import org.openhab.binding.shelly.internal.config.ShellyThingConfiguration;
import org.openhab.core.library.unit.ImperialUnits;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
import com.google.gson.JsonSyntaxException;
import tec.uom.se.unit.Units;
/**
* {@link ShellyHttpApi} wraps the Shelly REST API and provides various low level function to access the device api (not
* cloud api).
*
* @author Markus Michels - Initial contribution
*/
@NonNullByDefault
public class ShellyHttpApi {
public static final String HTTP_HEADER_AUTH = "Authorization";
public static final String HTTP_AUTH_TYPE_BASIC = "Basic";
public static final String CONTENT_TYPE_JSON = "application/json; charset=UTF-8";
private final Logger logger = LoggerFactory.getLogger(ShellyHttpApi.class);
private final HttpClient httpClient;
private ShellyThingConfiguration config = new ShellyThingConfiguration();
private String thingName;
private final Gson gson = new Gson();
private int timeoutErrors = 0;
private int timeoutsRecovered = 0;
private ShellyDeviceProfile profile = new ShellyDeviceProfile();
public ShellyHttpApi(String thingName, ShellyThingConfiguration config, HttpClient httpClient) {
this.httpClient = httpClient;
this.thingName = thingName;
setConfig(thingName, config);
profile.initFromThingType(thingName);
}
public void setConfig(String thingName, ShellyThingConfiguration config) {
this.thingName = thingName;
this.config = config;
}
public ShellySettingsDevice getDevInfo() throws ShellyApiException {
return callApi(SHELLY_URL_DEVINFO, ShellySettingsDevice.class);
}
/**
* Initialize the device profile
*
* @param thingType Type of DEVICE as returned from the thing properties (based on discovery)
* @return Initialized ShellyDeviceProfile
* @throws ShellyApiException
*/
public ShellyDeviceProfile getDeviceProfile(String thingType) throws ShellyApiException {
String json = request(SHELLY_URL_SETTINGS);
if (json.contains("\"type\":\"SHDM-")) {
logger.trace("{}: Detected a Shelly Dimmer: fix Json (replace lights[] tag with dimmers[]", thingName);
json = fixDimmerJson(json);
}
// Map settings to device profile for Light and Sense
profile.initialize(thingType, json);
// 2nd level initialization
profile.thingName = profile.hostname;
if (profile.isLight && (profile.numMeters == 0)) {
logger.debug("{}: Get number of meters from light status", thingName);
ShellyStatusLight status = getLightStatus();
profile.numMeters = status.meters != null ? status.meters.size() : 0;
}
if (profile.isSense) {
profile.irCodes = getIRCodeList();
logger.debug("{}: Sense stored key list loaded, {} entries.", thingName, profile.irCodes.size());
}
return profile;
}
public boolean isInitialized() {
return profile.initialized;
}
/**
* Get generic device settings/status. Json returned from API will be mapped to a Gson object
*
* @return Device settings/status as ShellySettingsStatus object
* @throws ShellyApiException
*/
public ShellySettingsStatus getStatus() throws ShellyApiException {
String json = "";
try {
json = request(SHELLY_URL_STATUS);
// Dimmer2 returns invalid json type for loaderror :-(
json = json.replace("\"loaderror\":0,", "\"loaderror\":false,");
json = json.replace("\"loaderror\":1,", "\"loaderror\":true,");
ShellySettingsStatus status = gson.fromJson(json, ShellySettingsStatus.class);
status.json = json;
return status;
} catch (JsonSyntaxException e) {
throw new ShellyApiException("Unable to parse JSON: " + json, e);
}
}
public ShellyStatusRelay getRelayStatus(Integer relayIndex) throws ShellyApiException {
return callApi(SHELLY_URL_STATUS_RELEAY + "/" + relayIndex.toString(), ShellyStatusRelay.class);
}
public ShellyShortLightStatus setRelayTurn(Integer id, String turnMode) throws ShellyApiException {
return callApi(getControlUriPrefix(id) + "?" + SHELLY_LIGHT_TURN + "=" + turnMode.toLowerCase(),
ShellyShortLightStatus.class);
}
public void setBrightness(Integer id, Integer brightness, boolean autoOn) throws ShellyApiException {
String turn = autoOn ? SHELLY_LIGHT_TURN + "=" + SHELLY_API_ON + "&" : "";
request(getControlUriPrefix(id) + "?" + turn + "brightness=" + brightness.toString());
}
public ShellyControlRoller getRollerStatus(Integer rollerIndex) throws ShellyApiException {
String uri = SHELLY_URL_CONTROL_ROLLER + "/" + rollerIndex.toString() + "/pos";
return callApi(uri, ShellyControlRoller.class);
}
public void setRollerTurn(Integer relayIndex, String turnMode) throws ShellyApiException {
request(SHELLY_URL_CONTROL_ROLLER + "/" + relayIndex.toString() + "?go=" + turnMode);
}
public void setRollerPos(Integer relayIndex, Integer position) throws ShellyApiException {
request(SHELLY_URL_CONTROL_ROLLER + "/" + relayIndex.toString() + "?go=to_pos&roller_pos="
+ position.toString());
}
public void setRollerTimer(Integer relayIndex, Integer timer) throws ShellyApiException {
request(SHELLY_URL_CONTROL_ROLLER + "/" + relayIndex.toString() + "?timer=" + timer.toString());
}
public ShellyShortLightStatus getLightStatus(Integer index) throws ShellyApiException {
return callApi(getControlUriPrefix(index), ShellyShortLightStatus.class);
}
public ShellyStatusSensor getSensorStatus() throws ShellyApiException {
ShellyStatusSensor status = callApi(SHELLY_URL_STATUS, ShellyStatusSensor.class);
if (profile.isSense) {
// complete reported data, map C to F or vice versa: C=(F - 32) * 0.5556;
status.tmp.tC = status.tmp.units.equals(SHELLY_TEMP_CELSIUS) ? status.tmp.value
: ImperialUnits.FAHRENHEIT.getConverterTo(Units.CELSIUS).convert(getDouble(status.tmp.value))
.doubleValue();
status.tmp.tF = status.tmp.units.equals(SHELLY_TEMP_FAHRENHEIT) ? status.tmp.value
: Units.CELSIUS.getConverterTo(ImperialUnits.FAHRENHEIT).convert(getDouble(status.tmp.value))
.doubleValue();
}
if ((status.charger == null) && (status.externalPower != null)) {
// SHelly H&T uses external_power, Sense uses charger
status.charger = status.externalPower != 0;
}
return status;
}
public void setTimer(Integer index, String timerName, Double value) throws ShellyApiException {
String type = SHELLY_CLASS_RELAY;
if (profile.isRoller) {
type = SHELLY_CLASS_ROLLER;
} else if (profile.isLight) {
type = SHELLY_CLASS_LIGHT;
}
String uri = SHELLY_URL_SETTINGS + "/" + type + "/" + index + "?" + timerName + "="
+ ((Integer) value.intValue()).toString();
request(uri);
}
public void setLedStatus(String ledName, Boolean value) throws ShellyApiException {
request(SHELLY_URL_SETTINGS + "?" + ledName + "=" + (value ? SHELLY_API_TRUE : SHELLY_API_FALSE));
}
public ShellySettingsLight getLightSettings() throws ShellyApiException {
return callApi(SHELLY_URL_SETTINGS_LIGHT, ShellySettingsLight.class);
}
public ShellyStatusLight getLightStatus() throws ShellyApiException {
return callApi(SHELLY_URL_STATUS, ShellyStatusLight.class);
}
public void setLightSetting(String parm, String value) throws ShellyApiException {
request(SHELLY_URL_SETTINGS + "?" + parm + "=" + value);
}
/**
* Change between White and Color Mode
*
* @param mode
* @throws ShellyApiException
*/
public void setLightMode(String mode) throws ShellyApiException {
if (!mode.isEmpty() && !profile.mode.equals(mode)) {
setLightSetting(SHELLY_API_MODE, mode);
profile.mode = mode;
profile.inColor = profile.isLight && profile.mode.equalsIgnoreCase(SHELLY_MODE_COLOR);
}
}
/**
* Set a single light parameter
*
* @param lightIndex Index of the light, usually 0 for Bulb and 0..3 for RGBW2.
* @param parm Name of the parameter (see API spec)
* @param value The value
* @throws ShellyApiException
*/
public void setLightParm(Integer lightIndex, String parm, String value) throws ShellyApiException {
// Bulb, RGW2: /<color mode>/<light id>?parm?value
// Dimmer: /light/<light id>?parm=value
request(getControlUriPrefix(lightIndex) + "?" + parm + "=" + value);
}
public void setLightParms(Integer lightIndex, Map<String, String> parameters) throws ShellyApiException {
String url = getControlUriPrefix(lightIndex) + "?";
int i = 0;
for (String key : parameters.keySet()) {
if (i > 0) {
url = url + "&";
}
url = url + key + "=" + parameters.get(key);
i++;
}
request(url);
}
/**
* Retrieve the IR Code list from the Shelly Sense device. The list could be customized by the user. It defines the
* symbolic key code, which gets
* map into a PRONTO code
*
* @return Map of key codes
* @throws ShellyApiException
*/
public Map<String, String> getIRCodeList() throws ShellyApiException {
String result = request(SHELLY_URL_LIST_IR);
// take pragmatic approach to make the returned JSon into named arrays for Gson parsing
String keyList = substringAfter(result, "[");
keyList = substringBeforeLast(keyList, "]");
keyList = keyList.replaceAll(java.util.regex.Pattern.quote("\",\""), "\", \"name\": \"");
keyList = keyList.replaceAll(java.util.regex.Pattern.quote("["), "{ \"id\":");
keyList = keyList.replaceAll(java.util.regex.Pattern.quote("]"), "} ");
String json = "{\"key_codes\" : [" + keyList + "] }";
ShellySendKeyList codes = gson.fromJson(json, ShellySendKeyList.class);
Map<String, String> list = new HashMap<>();
for (ShellySenseKeyCode key : codes.keyCodes) {
list.put(key.id, key.name);
}
return list;
}
/**
* Sends a IR key code to the Shelly Sense.
*
* @param keyCode A keyCoud could be a symbolic name (as defined in the key map on the device) or a PRONTO Code in
* plain or hex64 format
*
* @throws ShellyApiException
* @throws IllegalArgumentException
*/
public void sendIRKey(String keyCode) throws ShellyApiException, IllegalArgumentException {
String type = "";
if (profile.irCodes.containsKey(keyCode)) {
type = SHELLY_IR_CODET_STORED;
} else if ((keyCode.length() > 4) && keyCode.contains(" ")) {
type = SHELLY_IR_CODET_PRONTO;
} else {
type = SHELLY_IR_CODET_PRONTO_HEX;
}
String url = SHELLY_URL_SEND_IR + "?type=" + type;
if (type.equals(SHELLY_IR_CODET_STORED)) {
url = url + "&" + "id=" + keyCode;
} else if (type.equals(SHELLY_IR_CODET_PRONTO)) {
String code = Base64.getEncoder().encodeToString(keyCode.getBytes(StandardCharsets.UTF_8));
if (code == null) {
throw new IllegalArgumentException("Unable to BASE64 encode the pronto code: " + keyCode);
}
url = url + "&" + SHELLY_IR_CODET_PRONTO + "=" + code;
} else if (type.equals(SHELLY_IR_CODET_PRONTO_HEX)) {
url = url + "&" + SHELLY_IR_CODET_PRONTO_HEX + "=" + keyCode;
}
request(url);
}
public void setSenseSetting(String setting, String value) throws ShellyApiException {
request(SHELLY_URL_SETTINGS + "?" + setting + "=" + value);
}
/**
* Set event callback URLs. Depending on the device different event types are supported. In fact all of them will be
* redirected to the binding's servlet and act as a trigger to schedule a status update
*
* @param ShellyApiException
* @throws ShellyApiException
*/
public void setActionURLs() throws ShellyApiException {
setRelayEvents();
setDimmerEvents();
setSensorEventUrls();
}
private void setRelayEvents() throws ShellyApiException {
if (profile.settings.relays != null) {
int num = profile.isRoller ? profile.numRollers : profile.numRelays;
for (int i = 0; i < num; i++) {
setEventUrls(i);
}
}
}
private void setDimmerEvents() throws ShellyApiException {
if (profile.settings.dimmers != null) {
for (int i = 0; i < profile.settings.dimmers.size(); i++) {
setEventUrls(i);
}
} else if (profile.isLight) {
setEventUrls(0);
}
}
/**
* Set sensor Action URLs
*
* @throws ShellyApiException
*/
private void setSensorEventUrls() throws ShellyApiException, ShellyApiException {
if (profile.isSensor) {
logger.debug("{}: Set Sensor Reporting URL", thingName);
setEventUrl(config.eventsSensorReport, SHELLY_EVENT_SENSORREPORT, SHELLY_EVENT_DARK, SHELLY_EVENT_TWILIGHT,
SHELLY_EVENT_FLOOD_DETECTED, SHELLY_EVENT_FLOOD_GONE, SHELLY_EVENT_OPEN, SHELLY_EVENT_CLOSE,
SHELLY_EVENT_VIBRATION, SHELLY_EVENT_ALARM_MILD, SHELLY_EVENT_ALARM_HEAVY, SHELLY_EVENT_ALARM_OFF,
SHELLY_EVENT_TEMP_OVER, SHELLY_EVENT_TEMP_UNDER);
}
}
/**
* Set/delete Relay/Roller/Dimmer Action URLs
*
* @param index Device Index (0-based)
* @throws ShellyApiException
*/
private void setEventUrls(Integer index) throws ShellyApiException {
if (profile.isRoller) {
setEventUrl(EVENT_TYPE_ROLLER, 0, config.eventsRoller, SHELLY_EVENT_ROLLER_OPEN, SHELLY_EVENT_ROLLER_CLOSE,
SHELLY_EVENT_ROLLER_STOP);
} else if (profile.isDimmer) {
// 2 set of URLs
setEventUrl(EVENT_TYPE_LIGHT, index, config.eventsButton, SHELLY_EVENT_BTN1_ON, SHELLY_EVENT_BTN1_OFF,
SHELLY_EVENT_BTN2_ON, SHELLY_EVENT_BTN2_OFF);
setEventUrl(EVENT_TYPE_LIGHT, index, config.eventsPush, SHELLY_EVENT_SHORTPUSH1, SHELLY_EVENT_LONGPUSH1,
SHELLY_EVENT_SHORTPUSH2, SHELLY_EVENT_LONGPUSH2);
// Relay output
setEventUrl(EVENT_TYPE_LIGHT, index, config.eventsSwitch, SHELLY_EVENT_OUT_ON, SHELLY_EVENT_OUT_OFF);
} else if (profile.hasRelays) {
// Standard relays: btn_xxx, out_xxx, short/longpush URLs
setEventUrl(EVENT_TYPE_RELAY, index, config.eventsButton, SHELLY_EVENT_BTN_ON, SHELLY_EVENT_BTN_OFF);
setEventUrl(EVENT_TYPE_RELAY, index, config.eventsPush, SHELLY_EVENT_SHORTPUSH, SHELLY_EVENT_LONGPUSH);
setEventUrl(EVENT_TYPE_RELAY, index, config.eventsSwitch, SHELLY_EVENT_OUT_ON, SHELLY_EVENT_OUT_OFF);
} else if (profile.isLight) {
// Duo, Bulb
setEventUrl(EVENT_TYPE_LIGHT, index, config.eventsSwitch, SHELLY_EVENT_OUT_ON, SHELLY_EVENT_OUT_OFF);
}
}
private void setEventUrl(boolean enabled, String... eventTypes) throws ShellyApiException {
if (config.localIp.isEmpty()) {
throw new ShellyApiException(thingName + ": Local IP address was not detected, can't build Callback URL");
}
for (String eventType : eventTypes) {
if (profile.containsEventUrl(eventType)) {
// H&T adds the type=xx to report_url itself, so we need to ommit here
String eclass = profile.isSensor ? EVENT_TYPE_SENSORDATA : eventType;
String urlParm = eventType.contains("temp") || profile.isHT ? "" : "?type=" + eventType;
String callBackUrl = "http://" + config.localIp + ":" + config.localPort + SHELLY_CALLBACK_URI + "/"
+ profile.thingName + "/" + eclass + urlParm;
String newUrl = enabled ? callBackUrl : SHELLY_NULL_URL;
String testUrl = "\"" + mkEventUrl(eventType) + "\":\"" + newUrl + "\"";
if (!enabled && !profile.settingsJson.contains(testUrl)) {
// Don't set URL to null when the current one doesn't point to this OH
// Don't interfere with a 3rd party App
continue;
}
if (!profile.settingsJson.contains(testUrl)) {
// Current Action URL is != new URL
logger.debug("{}: Set new url for event type {}: {}", thingName, eventType, newUrl);
request(SHELLY_URL_SETTINGS + "?" + mkEventUrl(eventType) + "=" + urlEncode(newUrl));
}
}
}
}
private void setEventUrl(String deviceClass, Integer index, boolean enabled, String... eventTypes)
throws ShellyApiException {
for (String eventType : eventTypes) {
if (profile.containsEventUrl(eventType)) {
String callBackUrl = "http://" + config.localIp + ":" + config.localPort + SHELLY_CALLBACK_URI + "/"
+ profile.thingName + "/" + deviceClass + "/" + index + "?type=" + eventType;
String newUrl = enabled ? callBackUrl : SHELLY_NULL_URL;
String test = "\"" + mkEventUrl(eventType) + "\":\"" + callBackUrl + "\"";
if (!enabled && !profile.settingsJson.contains(test)) {
// Don't set URL to null when the current one doesn't point to this OH
// Don't interfere with a 3rd party App
continue;
}
test = "\"" + mkEventUrl(eventType) + "\":\"" + newUrl + "\"";
if (!profile.settingsJson.contains(test)) {
// Current Action URL is != new URL
logger.debug("{}: Set URL for type {} to {}", thingName, eventType, newUrl);
request(SHELLY_URL_SETTINGS + "/" + deviceClass + "/" + index + "?" + mkEventUrl(eventType) + "="
+ urlEncode(newUrl));
}
}
}
}
private static String mkEventUrl(String eventType) {
return eventType + SHELLY_EVENTURL_SUFFIX;
}
/**
* Submit GET request and return response, check for invalid responses
*
* @param uri: URI (e.g. "/settings")
*/
public <T> T callApi(String uri, Class<T> classOfT) throws ShellyApiException {
try {
String json = request(uri);
return gson.fromJson(json, classOfT);
} catch (JsonSyntaxException e) {
throw new ShellyApiException("Unable to convert JSON", e);
}
}
private String request(String uri) throws ShellyApiException {
ShellyApiResult apiResult = new ShellyApiResult();
int retries = 3;
boolean timeout = false;
while (retries > 0) {
try {
apiResult = innerRequest(HttpMethod.GET, uri);
if (timeout) {
logger.debug("{}: API timeout #{}/{} recovered ({})", thingName, timeoutErrors, timeoutsRecovered,
apiResult.getUrl());
timeoutsRecovered++;
}
return apiResult.response; // successful
} catch (ShellyApiException e) {
if ((!e.isTimeout() && !apiResult.isHttpServerError()) || profile.hasBattery || (retries == 0)) {
// Sensor in sleep mode or API exception for non-battery device or retry counter expired
throw e; // non-timeout exception
}
timeout = true;
retries--;
timeoutErrors++; // count the retries
logger.debug("{}: API Timeout, retry #{} ({})", thingName, timeoutErrors, e.toString());
}
}
throw new ShellyApiException("Inconsistent API result or Timeout"); // successful
}
private ShellyApiResult innerRequest(HttpMethod method, String uri) throws ShellyApiException {
Request request = null;
String url = "http://" + config.deviceIp + uri;
ShellyApiResult apiResult = new ShellyApiResult(method.toString(), url);
try {
request = httpClient.newRequest(url).method(method.toString()).timeout(SHELLY_API_TIMEOUT_MS,
TimeUnit.MILLISECONDS);
if (!config.userId.isEmpty()) {
String value = config.userId + ":" + config.password;
request.header(HTTP_HEADER_AUTH,
HTTP_AUTH_TYPE_BASIC + " " + Base64.getEncoder().encodeToString(value.getBytes()));
}
request.header(HttpHeader.ACCEPT, CONTENT_TYPE_JSON);
logger.trace("{}: HTTP {} for {}", thingName, method, url);
// Do request and get response
ContentResponse contentResponse = request.send();
apiResult = new ShellyApiResult(contentResponse);
String response = contentResponse.getContentAsString().replace("\t", "").replace("\r\n", "").trim();
logger.trace("{}: HTTP Response {}: {}", thingName, contentResponse.getStatus(), response);
// validate response, API errors are reported as Json
if (contentResponse.getStatus() != HttpStatus.OK_200) {
throw new ShellyApiException(apiResult);
}
if (response == null || response.isEmpty() || !response.startsWith("{") && !response.startsWith("[")) {
throw new ShellyApiException("Unexpected response: " + response);
}
} catch (ExecutionException | InterruptedException | TimeoutException | IllegalArgumentException e) {
ShellyApiException ex = new ShellyApiException(apiResult, e);
if (!ex.isTimeout()) { // will be handled by the caller
logger.trace("{}: API call returned exception", thingName, ex);
}
throw ex;
}
return apiResult;
}
public String getControlUriPrefix(Integer id) {
String uri = "";
if (profile.isLight || profile.isDimmer) {
if (profile.isDuo || profile.isDimmer) {
// Duo + Dimmer
uri = SHELLY_URL_CONTROL_LIGHT;
} else {
// Bulb + RGBW2
uri = "/" + (profile.inColor ? SHELLY_MODE_COLOR : SHELLY_MODE_WHITE);
}
} else {
// Roller, Relay
uri = SHELLY_URL_CONTROL_RELEAY;
}
uri = uri + "/" + id;
return uri;
}
public int getTimeoutErrors() {
return timeoutErrors;
}
public int getTimeoutsRecovered() {
return timeoutsRecovered;
}
}

View File

@@ -0,0 +1,37 @@
/**
* Copyright (c) 2010-2020 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.shelly.internal.coap;
import java.util.List;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO.CoIotDescrBlk;
import org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO.CoIotDescrSen;
import org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO.CoIotSensor;
import org.openhab.core.types.State;
/**
* The {@link ShellyCoapListener} describes the listening interface to process Coap responses
*
* @author Markus Michels - Initial contribution
*/
@NonNullByDefault
public interface ShellyCoIoTInterface {
public int getVersion();
public CoIotDescrSen fixDescription(CoIotDescrSen sen, Map<String, CoIotDescrBlk> blkMap);
public boolean handleStatusUpdate(List<CoIotSensor> sensorUpdates, CoIotDescrSen sen, CoIotSensor s,
Map<String, State> updates);
}

View File

@@ -0,0 +1,355 @@
/**
* Copyright (c) 2010-2020 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.shelly.internal.coap;
import static org.openhab.binding.shelly.internal.ShellyBindingConstants.*;
import static org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.*;
import static org.openhab.binding.shelly.internal.util.ShellyUtils.*;
import java.util.List;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.shelly.internal.api.ShellyDeviceProfile;
import org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO.CoIotDescrBlk;
import org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO.CoIotDescrSen;
import org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO.CoIotSensor;
import org.openhab.binding.shelly.internal.handler.ShellyBaseHandler;
import org.openhab.binding.shelly.internal.handler.ShellyColorUtils;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.OpenClosedType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.library.unit.SmartHomeUnits;
import org.openhab.core.types.State;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link ShellyCoIoTProtocol} implements common functions for the CoIoT implementations
*
* @author Markus Michels - Initial contribution
*/
@NonNullByDefault
public class ShellyCoIoTProtocol {
private final Logger logger = LoggerFactory.getLogger(ShellyCoIoTProtocol.class);
protected final String thingName;
protected final ShellyBaseHandler thingHandler;
protected final ShellyDeviceProfile profile;
protected final Map<String, CoIotDescrBlk> blkMap;
protected final Map<String, CoIotDescrSen> sensorMap;
// Due to the fact that the device reports only the current/last status, but no real events, we need to distinguish
// between a real update or just a repeated status on periodic updates
protected int lastCfgCount = -1;
protected int[] lastEventCount = { -1, -1, -1, -1, -1, -1, -1, -1 }; // 4Pro has 4 relays, so 8 should be fine
protected String[] inputEvent = { "", "", "", "", "", "", "", "" };
public ShellyCoIoTProtocol(String thingName, ShellyBaseHandler thingHandler, Map<String, CoIotDescrBlk> blkMap,
Map<String, CoIotDescrSen> sensorMap) {
this.thingName = thingName;
this.thingHandler = thingHandler;
this.blkMap = blkMap;
this.sensorMap = sensorMap;
this.profile = thingHandler.getProfile();
}
protected boolean handleStatusUpdate(List<CoIotSensor> sensorUpdates, CoIotDescrSen sen, CoIotSensor s,
Map<String, State> updates) {
// Process status information and convert into channel updates
// Integer rIndex = Integer.parseInt(sen.links) + 1;
// String rGroup = getProfile().numRelays <= 1 ? CHANNEL_GROUP_RELAY_CONTROL
// : CHANNEL_GROUP_RELAY_CONTROL + rIndex;
int rIndex = getIdFromBlk(sen);
String rGroup = getProfile().numRelays <= 1 ? CHANNEL_GROUP_RELAY_CONTROL
: CHANNEL_GROUP_RELAY_CONTROL + rIndex;
switch (sen.type.toLowerCase()) {
case "b": // BatteryLevel +
updateChannel(updates, CHANNEL_GROUP_BATTERY, CHANNEL_SENSOR_BAT_LEVEL,
toQuantityType(s.value, DIGITS_PERCENT, SmartHomeUnits.PERCENT));
break;
case "h" /* Humidity */:
updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_HUM,
toQuantityType(s.value, DIGITS_PERCENT, SmartHomeUnits.PERCENT));
break;
case "m" /* Motion */:
updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_MOTION,
s.value == 1 ? OnOffType.ON : OnOffType.OFF);
break;
case "l": // Luminosity +
updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_LUX,
toQuantityType(s.value, DIGITS_LUX, SmartHomeUnits.LUX));
break;
case "s": // CatchAll
switch (sen.desc.toLowerCase()) {
case "state": // Relay status +
case "output":
updatePower(profile, updates, rIndex, sen, s, sensorUpdates);
break;
case "input":
handleInput(sen, s, rGroup, updates);
break;
case "brightness":
// already handled by state/output
break;
case "overtemp": // ++
if (s.value == 1) {
thingHandler.postEvent(ALARM_TYPE_OVERTEMP, true);
}
break;
case "position":
// work around: Roller reports 101% instead max 100
double pos = Math.max(SHELLY_MIN_ROLLER_POS, Math.min(s.value, SHELLY_MAX_ROLLER_POS));
updateChannel(updates, CHANNEL_GROUP_ROL_CONTROL, CHANNEL_ROL_CONTROL_CONTROL,
toQuantityType(SHELLY_MAX_ROLLER_POS - pos, SmartHomeUnits.PERCENT));
updateChannel(updates, CHANNEL_GROUP_ROL_CONTROL, CHANNEL_ROL_CONTROL_POS,
toQuantityType(pos, SmartHomeUnits.PERCENT));
break;
case "flood":
updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_FLOOD,
s.value == 1 ? OnOffType.ON : OnOffType.OFF);
break;
case "vibration": // DW with FW1.6.5+
updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_VIBRATION,
s.value == 1 ? OnOffType.ON : OnOffType.OFF);
break;
case "luminositylevel": // +
updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_ILLUM, getStringType(s.valueStr));
break;
case "charger": // Sense
updateChannel(updates, CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_CHARGER,
s.value == 1 ? OnOffType.ON : OnOffType.OFF);
break;
// RGBW2/Bulb
case "red":
updateChannel(updates, CHANNEL_GROUP_COLOR_CONTROL, CHANNEL_COLOR_RED,
ShellyColorUtils.toPercent((int) s.value));
break;
case "green":
updateChannel(updates, CHANNEL_GROUP_COLOR_CONTROL, CHANNEL_COLOR_GREEN,
ShellyColorUtils.toPercent((int) s.value));
break;
case "blue":
updateChannel(updates, CHANNEL_GROUP_COLOR_CONTROL, CHANNEL_COLOR_BLUE,
ShellyColorUtils.toPercent((int) s.value));
break;
case "white":
updateChannel(updates, CHANNEL_GROUP_COLOR_CONTROL, CHANNEL_COLOR_WHITE,
ShellyColorUtils.toPercent((int) s.value));
break;
case "gain":
updateChannel(updates, CHANNEL_GROUP_COLOR_CONTROL, CHANNEL_COLOR_GAIN,
ShellyColorUtils.toPercent((int) s.value, SHELLY_MIN_GAIN, SHELLY_MAX_GAIN));
break;
case "sensorerror": // +
updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_ERROR, getStringType(s.valueStr));
break;
default:
// Unknown
return false;
}
break;
default:
// Unknown type
return false;
}
return true;
}
protected boolean updateChannel(Map<String, State> updates, String group, String channel, State value) {
updates.put(mkChannelId(group, channel), value);
return true;
}
protected void handleInput(CoIotDescrSen sen, CoIotSensor s, String rGroup, Map<String, State> updates) {
int idx = getSensorNumber(sen.desc, sen.id) - 1;
String iGroup = profile.getInputGroup(idx);
String iChannel = profile.getInputChannel(idx);
updateChannel(updates, iGroup, iChannel, s.value == 0 ? OnOffType.OFF : OnOffType.ON);
}
protected void handleInputEvent(CoIotDescrSen sen, String type, Integer count, Map<String, State> updates) {
int idx = getSensorNumber(sen.desc, sen.id) - 1;
String group = profile.getInputGroup(idx);
if (count == -1) {
// event type
updateChannel(updates, group, CHANNEL_STATUS_EVENTTYPE, new StringType(type));
inputEvent[idx] = type;
} else {
// event count
updateChannel(updates, group, CHANNEL_STATUS_EVENTCOUNT, getDecimal(count));
if (profile.inButtonMode(idx) && ((profile.hasBattery && (count == 1)) || (count != lastEventCount[idx]))) {
if (profile.isButton || (lastEventCount[idx] != -1)) { // skip the first one if binding was restarted
thingHandler.triggerButton(group, inputEvent[idx]);
}
lastEventCount[idx] = count;
}
}
}
/**
*
* Handles the combined updated of the brightness channel:
* brightness$Switch is the OnOffType (power state)
* brightness&Value is the brightness value
*
* @param profile Device profile, required to select the channel group and name
* @param updates List of updates. updatePower will add brightness$Switch and brightness&Value if changed
* @param id Sensor id from the update
* @param sen Sensor description from the update
* @param s New sensor value
* @param allUpdatesList of updates. This is required, because we need to update both values at the same time
*/
protected void updatePower(ShellyDeviceProfile profile, Map<String, State> updates, int id, CoIotDescrSen sen,
CoIotSensor s, List<CoIotSensor> allUpdates) {
String group = "";
String channel = CHANNEL_BRIGHTNESS;
String checkL = ""; // RGBW-white uses 4 different Power, Brightness, VSwitch values
if (profile.isLight || profile.isDimmer) {
if (profile.isBulb || profile.inColor) {
group = CHANNEL_GROUP_LIGHT_CONTROL;
channel = CHANNEL_LIGHT_POWER;
} else if (profile.isDuo) {
group = CHANNEL_GROUP_WHITE_CONTROL;
} else if (profile.isDimmer) {
group = CHANNEL_GROUP_RELAY_CONTROL;
} else if (profile.isRGBW2) {
group = CHANNEL_GROUP_LIGHT_CHANNEL + id;
checkL = String.valueOf(id - 1); // id is 1-based, L is 0-based
logger.trace("{}: updatePower() for L={}", thingName, checkL);
}
// We need to update brigthtess and on/off state at the same time to avoid "flipping brightness slider" in
// the UI
Double brightness = -1.0;
Double power = -1.0;
for (CoIotSensor update : allUpdates) {
CoIotDescrSen d = fixDescription(sensorMap.get(update.id), blkMap);
if (!checkL.isEmpty() && !d.links.equals(checkL)) {
// continue until we find the correct one
continue;
}
if (d.desc.equalsIgnoreCase("brightness")) {
brightness = new Double(update.value);
} else if (d.desc.equalsIgnoreCase("output") || d.desc.equalsIgnoreCase("state")) {
power = new Double(update.value);
}
}
if (power != -1) {
updateChannel(updates, group, channel + "$Switch", power == 1 ? OnOffType.ON : OnOffType.OFF);
}
if (brightness != -1) {
updateChannel(updates, group, channel + "$Value",
toQuantityType(power == 1 ? brightness : 0, DIGITS_NONE, SmartHomeUnits.PERCENT));
}
} else if (profile.hasRelays) {
group = profile.numRelays <= 1 ? CHANNEL_GROUP_RELAY_CONTROL : CHANNEL_GROUP_RELAY_CONTROL + id;
updateChannel(updates, group, CHANNEL_OUTPUT, s.value == 1 ? OnOffType.ON : OnOffType.OFF);
} else if (profile.isSensor) {
// Sensor state
if (profile.isDW) { // Door Window has item type Contact
updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_CONTACT,
s.value != 0 ? OpenClosedType.OPEN : OpenClosedType.CLOSED);
} else {
updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_CONTACT,
s.value == 1 ? OnOffType.ON : OnOffType.OFF);
}
}
}
/**
* Find index of Input id, which is required to map to channel name
*
* @parm sensorDesc D field from sensor update
* @param sensorId The id from the sensor update
* @return Index of found entry (+1 will be the suffix for the channel name) or null if sensorId is not found
*/
protected int getSensorNumber(String sensorDesc, String sensorId) {
int idx = 0;
for (Map.Entry<String, CoIotDescrSen> se : sensorMap.entrySet()) {
CoIotDescrSen sen = se.getValue();
if (sen.desc.equalsIgnoreCase(sensorDesc)) {
idx++; // iterate from input1..2..n
}
if (sen.id.equalsIgnoreCase(sensorId) && blkMap.containsKey(sen.links)) {
int id = getIdFromBlk(sen);
if (id != -1) {
return id;
}
}
if (sen.id.equalsIgnoreCase(sensorId)) {
return idx;
}
}
logger.debug("{}: sensorId {} not found in sensorMap!", thingName, sensorId);
return -1;
}
protected int getIdFromBlk(CoIotDescrSen sen) {
int idx = -1;
if (blkMap.containsKey(sen.links)) {
CoIotDescrBlk blk = blkMap.get(sen.links);
String desc = blk.desc.toLowerCase();
if (desc.startsWith(SHELLY_CLASS_RELAY) || desc.startsWith(SHELLY_CLASS_ROLLER)
|| desc.startsWith(SHELLY_CLASS_EMETER)) {
if (desc.contains("_")) { // CoAP v2
idx = Integer.parseInt(substringAfter(desc, "_"));
} else { // CoAP v1
if (desc.substring(0, 5).equalsIgnoreCase(SHELLY_CLASS_RELAY)) {
idx = Integer.parseInt(substringAfter(desc, SHELLY_CLASS_RELAY));
}
if (desc.substring(0, 6).equalsIgnoreCase(SHELLY_CLASS_ROLLER)) {
idx = Integer.parseInt(substringAfter(desc, SHELLY_CLASS_ROLLER));
}
if (desc.substring(0, SHELLY_CLASS_EMETER.length()).equalsIgnoreCase(SHELLY_CLASS_EMETER)) {
idx = Integer.parseInt(substringAfter(desc, SHELLY_CLASS_EMETER));
}
}
idx = idx + 1; // make it 1-based (sen.L is 0-based)
}
}
return idx;
}
/**
*
* Get matching sensorId for updates on "External Temperature" - there might be more than 1 sensor.
*
* @param sensorId sensorId to map into a channel index
* @return Index of the corresponding channel (e.g. 0 build temperature1, 1->temperagture2...)
*/
protected int getExtTempId(String sensorId) {
int idx = 0;
for (Map.Entry<String, CoIotDescrSen> se : sensorMap.entrySet()) {
CoIotDescrSen sen = se.getValue();
if (sen.desc.equalsIgnoreCase("external_temperature") || sen.desc.equalsIgnoreCase("external temperature c")
|| (sen.desc.equalsIgnoreCase("extTemp") && !sen.unit.equalsIgnoreCase(SHELLY_TEMP_FAHRENHEIT))) {
idx++; // iterate from temperature1..2..n
}
if (sen.id.equalsIgnoreCase(sensorId)) {
return idx;
}
}
logger.debug("{}: sensorId {} not found in sensorMap!", thingName, sensorId);
return -1;
}
protected ShellyDeviceProfile getProfile() {
return profile;
}
public CoIotDescrSen fixDescription(CoIotDescrSen sen, Map<String, CoIotDescrBlk> blkMap) {
return sen;
}
}

View File

@@ -0,0 +1,362 @@
/**
* Copyright (c) 2010-2020 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.shelly.internal.coap;
import static org.openhab.binding.shelly.internal.ShellyBindingConstants.*;
import static org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.*;
import static org.openhab.binding.shelly.internal.util.ShellyUtils.*;
import java.util.List;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO.CoIotDescrBlk;
import org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO.CoIotDescrSen;
import org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO.CoIotSensor;
import org.openhab.binding.shelly.internal.handler.ShellyBaseHandler;
import org.openhab.binding.shelly.internal.handler.ShellyColorUtils;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.unit.ImperialUnits;
import org.openhab.core.library.unit.SIUnits;
import org.openhab.core.library.unit.SmartHomeUnits;
import org.openhab.core.types.State;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import tec.uom.se.unit.Units;
/**
* The {@link ShellyCoIoTVersion1} implements the parsing for CoIoT version 1
*
* @author Markus Michels - Initial contribution
*/
@NonNullByDefault
public class ShellyCoIoTVersion1 extends ShellyCoIoTProtocol implements ShellyCoIoTInterface {
private final Logger logger = LoggerFactory.getLogger(ShellyCoIoTVersion1.class);
public ShellyCoIoTVersion1(String thingName, ShellyBaseHandler thingHandler, Map<String, CoIotDescrBlk> blkMap,
Map<String, CoIotDescrSen> sensorMap) {
super(thingName, thingHandler, blkMap, sensorMap);
}
@Override
public int getVersion() {
return ShellyCoapJSonDTO.COIOT_VERSION_1;
}
/**
* Process CoIoT status update message. If a status update is received, but the device description has not been
* received yet a GET is send to query device description.
*
* @param devId device id included in the status packet
* @param payload CoAP payload (Json format), example: {"G":[[0,112,0]]}
* @param serial Serial for this request. If this the the same as last serial
* the update was already sent and processed so this one gets
* ignored.
*/
@Override
public boolean handleStatusUpdate(List<CoIotSensor> sensorUpdates, CoIotDescrSen sen, CoIotSensor s,
Map<String, State> updates) {
// first check the base implementation
if (super.handleStatusUpdate(sensorUpdates, sen, s, updates)) {
// process by the base class
return true;
}
// Process status information and convert into channel updates
Integer rIndex = Integer.parseInt(sen.links) + 1;
String rGroup = getProfile().numRelays <= 1 ? CHANNEL_GROUP_RELAY_CONTROL
: CHANNEL_GROUP_RELAY_CONTROL + rIndex;
switch (sen.type.toLowerCase()) {
case "t": // Temperature +
Double value = getDouble(s.value);
switch (sen.desc.toLowerCase()) {
case "temperature": // Sensor Temp
if (getString(getProfile().settings.temperatureUnits)
.equalsIgnoreCase(SHELLY_TEMP_FAHRENHEIT)) {
value = ImperialUnits.FAHRENHEIT.getConverterTo(Units.CELSIUS).convert(getDouble(s.value))
.doubleValue();
}
updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_TEMP,
toQuantityType(value, DIGITS_TEMP, SIUnits.CELSIUS));
break;
case "temperature f": // Device Temp -> ignore (we use C only)
break;
case "temperature c": // Device Temp in C
// Device temperature
updateChannel(updates, CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_ITEMP,
toQuantityType(value, DIGITS_NONE, SIUnits.CELSIUS));
break;
case "external temperature f": // Shelly 1/1PM external temp sensors
// ignore F, we use C only
break;
case "external temperature c": // Shelly 1/1PM external temp sensors
case "external_temperature":
int idx = getExtTempId(sen.id);
if (idx > 0) {
updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_TEMP + idx,
toQuantityType(value, DIGITS_TEMP, SIUnits.CELSIUS));
} else {
logger.debug("{}: Unable to get extSensorId {} from {}/{}", thingName, sen.id, sen.type,
sen.desc);
}
break;
default:
logger.debug("{}: Unknown temperatur type: {}", thingName, sen.desc);
}
break;
case "p": // Power/Watt
// 3EM uses 1-based meter IDs, other 0-based
String mGroup = profile.numMeters == 1 ? CHANNEL_GROUP_METER
: CHANNEL_GROUP_METER + (profile.isEMeter ? sen.links : rIndex);
updateChannel(updates, mGroup, CHANNEL_METER_CURRENTWATTS,
toQuantityType(s.value, DIGITS_WATT, SmartHomeUnits.WATT));
updateChannel(updates, mGroup, CHANNEL_LAST_UPDATE, getTimestamp());
break;
case "s" /* CatchAll */:
switch (sen.desc.toLowerCase()) {
case "overtemp":
if (s.value == 1) {
thingHandler.postEvent(ALARM_TYPE_OVERTEMP, true);
}
break;
case "energy counter 0 [w-min]":
updateChannel(updates, rGroup, CHANNEL_METER_LASTMIN1,
toQuantityType(s.value, DIGITS_WATT, SmartHomeUnits.WATT));
break;
case "energy counter 1 [w-min]":
case "energy counter 2 [w-min]":
// we don't use them
break;
case "energy counter total [w-h]": // 3EM reports W/h
case "energy counter total [w-min]":
Double total = profile.isEMeter ? s.value / 1000 : s.value / 60 / 1000;
updateChannel(updates, rGroup, CHANNEL_METER_TOTALKWH,
toQuantityType(total, DIGITS_KWH, SmartHomeUnits.KILOWATT_HOUR));
break;
case "voltage":
updateChannel(updates, rGroup, CHANNEL_EMETER_VOLTAGE,
toQuantityType(getDouble(s.value), DIGITS_VOLT, SmartHomeUnits.VOLT));
break;
case "current":
updateChannel(updates, rGroup, CHANNEL_EMETER_CURRENT,
toQuantityType(getDouble(s.value), DIGITS_VOLT, SmartHomeUnits.AMPERE));
break;
case "pf":
updateChannel(updates, rGroup, CHANNEL_EMETER_PFACTOR, getDecimal(s.value));
break;
case "position":
// work around: Roller reports 101% instead max 100
double pos = Math.max(SHELLY_MIN_ROLLER_POS, Math.min(s.value, SHELLY_MAX_ROLLER_POS));
updateChannel(updates, CHANNEL_GROUP_ROL_CONTROL, CHANNEL_ROL_CONTROL_CONTROL,
toQuantityType(SHELLY_MAX_ROLLER_POS - pos, SmartHomeUnits.PERCENT));
updateChannel(updates, CHANNEL_GROUP_ROL_CONTROL, CHANNEL_ROL_CONTROL_POS,
toQuantityType(pos, SmartHomeUnits.PERCENT));
break;
case "input event": // Shelly Button 1
handleInputEvent(sen, getString(s.valueStr), -1, updates);
break;
case "input event counter": // Shelly Button 1/ix3
handleInputEvent(sen, "", getInteger((int) s.value), updates);
break;
case "flood":
updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_FLOOD,
s.value == 1 ? OnOffType.ON : OnOffType.OFF);
break;
case "tilt": // DW with FW1.6.5+ //+
updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_TILT,
toQuantityType(s.value, DIGITS_NONE, SmartHomeUnits.DEGREE_ANGLE));
break;
case "vibration": // DW with FW1.6.5+
updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_VIBRATION,
s.value == 1 ? OnOffType.ON : OnOffType.OFF);
break;
case "temp": // Shelly Bulb
case "colortemperature": // Shelly Duo
updateChannel(updates,
profile.inColor ? CHANNEL_GROUP_COLOR_CONTROL : CHANNEL_GROUP_WHITE_CONTROL,
CHANNEL_COLOR_TEMP,
ShellyColorUtils.toPercent((int) s.value, profile.minTemp, profile.maxTemp));
break;
case "sensor state": // Shelly Gas
updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_SSTATE, getStringType(s.valueStr));
break;
case "alarm state": // Shelly Gas
updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_ALARM_STATE,
getStringType(s.valueStr));
break;
case "self-test state":// Shelly Gas
updateChannel(updates, CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_SELFTTEST,
getStringType(s.valueStr));
break;
case "concentration":// Shelly Gas
updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_PPM, getDecimal(s.value));
break;
case "sensorerror":
updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_ERROR, getStringType(s.valueStr));
break;
default:
// Unknown
return false;
}
break;
default:
// Unknown type
return false;
}
return true;
}
/**
*
* Depending on the device type and firmware release there are significant bugs or incosistencies in the CoIoT
* Device Description returned by the discovery request. Shelly is even not following it's own speicifcation. All of
* that has been reported to Shelly and acknowledged. Firmware 1.6 brought significant improvements. However, the
* old mapping stays in to support older firmware releases.
*
* @param sen Sensor description received from device
* @return fixed Sensor description (sen)
*/
@Override
public CoIotDescrSen fixDescription(CoIotDescrSen sen, Map<String, CoIotDescrBlk> blkMap) {
// Shelly1: reports null descr+type "Switch" -> map to S
// Shelly1PM: reports null descr+type "Overtemp" -> map to O
// Shelly1PM: reports null descr+type "W" -> add description
// Shelly1PM: reports temp senmsors without desc -> add description
// Shelly Dimmer: sensors are reported without descriptions -> map to S
// SHelly Sense: multiple issues: Description should not be lower case, invalid type for Motion and Battery
// Shelly Sense: Battery is reported with Desc "battery", but type "H" instead of "B"
// Shelly Sense: Motion is reported with Desc "battery", but type "H" instead of "B"
// Shelly Bulb: Colors are coded with Type="Red" etc. rather than Type="S" and color as Descr
// Shelly RGBW2 is reporting Brightness, Power, VSwitch for each channel, but all with L=0
if (sen.desc == null) {
sen.desc = "";
}
String desc = sen.desc.toLowerCase();
// RGBW2 reports Power_0, Power_1, Power_2, Power_3; same for VSwitch and Brightness, all of them linkted to L:0
// we break it up to Power with L:0, Power with L:1...
if (desc.contains("_") && (desc.contains("power") || desc.contains("vswitch") || desc.contains("brightness"))) {
String newDesc = substringBefore(sen.desc, "_");
String newLink = substringAfter(sen.desc, "_");
sen.desc = newDesc;
sen.links = newLink;
if (!blkMap.containsKey(sen.links)) {
// auto-insert a matching blk entry
CoIotDescrBlk blk = new CoIotDescrBlk();
CoIotDescrBlk blk0 = blkMap.get("0"); // blk 0 is always there
blk.id = sen.links;
blk.desc = blk0.desc + "_" + blk.id;
blkMap.put(blk.id, blk);
}
}
switch (sen.type.toLowerCase()) {
case "w": // old devices/firmware releases use "W", new ones "P"
sen.type = "P";
sen.desc = "Power";
break;
case "tc":
sen.type = "T";
sen.desc = "Temperature C";
break;
case "tf":
sen.type = "T";
sen.desc = "Temperature F";
break;
case "overtemp":
sen.type = "S";
sen.desc = "Overtemp";
break;
case "relay0":
case "switch":
case "vswitch":
sen.type = "S";
sen.desc = "State";
break;
}
switch (sen.desc.toLowerCase()) {
case "motion": // fix acc to spec it's T=M
sen.type = "M";
sen.desc = "Motion";
break;
case "battery": // fix: type is B not H
sen.type = "B";
sen.desc = "Battery";
break;
case "overtemp":
sen.type = "S";
sen.desc = "Overtemp";
break;
case "relay0":
case "switch":
case "vswitch":
sen.type = "S";
sen.desc = "State";
break;
case "e cnt 0 [w-min]": // 4 Pro
case "e cnt 1 [w-min]":
case "e cnt 2 [w-min]":
case "e cnt total [w-min]": // 4 Pro
sen.desc = sen.desc.toLowerCase().replace("e cnt", "energy counter");
break;
}
if (sen.desc.isEmpty()) {
switch (sen.type.toLowerCase()) {
case "p":
sen.desc = "Power";
break;
case "T":
sen.desc = "Temperature";
break;
case "input":
sen.type = "S";
sen.desc = "Input";
break;
case "output":
sen.type = "S";
sen.desc = "Output";
break;
case "brightness":
sen.type = "S";
sen.desc = "Brightness";
break;
case "red":
case "green":
case "blue":
case "white":
case "gain":
case "temp": // Bulb: Color temperature
sen.desc = sen.type;
sen.type = "S";
break;
case "vswitch":
// it seems that Shelly tends to break their own spec: T is the description and D is no longer
// included -> map D to sen.T and set CatchAll for T
sen.desc = sen.type;
sen.type = "S";
break;
// Default: set no description
// (there are no T values defined in the CoIoT spec)
case "tostate":
default:
sen.desc = "";
}
}
return sen;
}
}

View File

@@ -0,0 +1,298 @@
/**
* Copyright (c) 2010-2020 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
*/
/**
* The {@link ShellyCoIoTVersion1} implements the parsing for CoIoT version 1
*
* @author Markus Michels - Initial contribution
*/
package org.openhab.binding.shelly.internal.coap;
import static org.openhab.binding.shelly.internal.ShellyBindingConstants.*;
import static org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.*;
import static org.openhab.binding.shelly.internal.util.ShellyUtils.*;
import java.util.List;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO.CoIotDescrBlk;
import org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO.CoIotDescrSen;
import org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO.CoIotSensor;
import org.openhab.binding.shelly.internal.handler.ShellyBaseHandler;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.OpenClosedType;
import org.openhab.core.library.unit.SIUnits;
import org.openhab.core.library.unit.SmartHomeUnits;
import org.openhab.core.types.State;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link ShellyCoIoTVersion1} implements the parsing for CoIoT version 2
*
* @author Markus Michels - Initial contribution
*/
@NonNullByDefault
public class ShellyCoIoTVersion2 extends ShellyCoIoTProtocol implements ShellyCoIoTInterface {
private final Logger logger = LoggerFactory.getLogger(ShellyCoIoTVersion2.class);
public ShellyCoIoTVersion2(String thingName, ShellyBaseHandler thingHandler, Map<String, CoIotDescrBlk> blkMap,
Map<String, CoIotDescrSen> sensorMap) {
super(thingName, thingHandler, blkMap, sensorMap);
}
@Override
public int getVersion() {
return ShellyCoapJSonDTO.COIOT_VERSION_2;
}
/**
* Process CoIoT status update message. If a status update is received, but the device description has not been
* received yet a GET is send to query device description.
*
* @param devId device id included in the status packet
* @param payload CoAP payload (Json format), example: {"G":[[0,112,0]]}
* @param serial Serial for this request. If this the the same as last serial
* the update was already sent and processed so this one gets
* ignored.
*/
@Override
public boolean handleStatusUpdate(List<CoIotSensor> sensorUpdates, CoIotDescrSen sen, CoIotSensor s,
Map<String, State> updates) {
// first check the base implementation
if (super.handleStatusUpdate(sensorUpdates, sen, s, updates)) {
// process by the base class
return true;
}
// Process status information and convert into channel updates
// Integer rIndex = Integer.parseInt(sen.links) + 1;
int rIndex = getIdFromBlk(sen);
String rGroup = getProfile().numRelays <= 1 ? CHANNEL_GROUP_RELAY_CONTROL
: CHANNEL_GROUP_RELAY_CONTROL + rIndex;
String mGroup = profile.numMeters == 1 ? CHANNEL_GROUP_METER
: CHANNEL_GROUP_METER + (profile.isEMeter ? getIdFromBlk(sen) : rIndex);
boolean processed = true;
double value = getDouble(s.value);
String reason = "";
switch (sen.id) {
case "3103": // H, humidity, 0-100 percent, unknown 999
case "3106": // L, luminosity, lux, U32, -1
case "3109": // S, tilt, 0-180deg, -1
case "3110": // S, luminosityLevel, dark/twilight/bright, "unknown"=unknown
case "3111": // B, battery, 0-100%, unknown -1
case "3112": // S, charger, 0/1
case "3115": // S, sensorError, 0/1
case "5101": // S, brightness, 1-100%
// processed by base handler
break;
case "6109": // P, overpowerValue, W, U32
case "9101":
// Relay: S, mode, relay/roller or
// Dimmer: S, mode, color/white
// skip, could check against thing mode...
break;
case "1101": // S, output, 0/1
updatePower(profile, updates, rIndex, sen, s, sensorUpdates);
break;
case "1102": // roler_0: S, roller, open/close/stop -> roller state
updateChannel(updates, CHANNEL_GROUP_ROL_CONTROL, CHANNEL_ROL_CONTROL_STATE, getStringType(s.valueStr));
break;
case "1103": // roller_0: S, rollerPos, 0-100, unknown -1
int pos = Math.max(SHELLY_MIN_ROLLER_POS, Math.min((int) value, SHELLY_MAX_ROLLER_POS));
updateChannel(updates, CHANNEL_GROUP_ROL_CONTROL, CHANNEL_ROL_CONTROL_CONTROL,
toQuantityType(new Double(SHELLY_MAX_ROLLER_POS - pos), SmartHomeUnits.PERCENT));
break;
case "1105": // S, valvle, closed/opened/not_connected/failure/closing/opening/checking or unbknown
updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_VALVE, getStringType(s.valueStr));
break;
case "2101": // Input_0: S, input, 0/1
case "2201": // Input_1: S, input, 0/1
case "2301": // Input_2: S, input, 0/1
case "2401": // Input_3: S, input, 0/1
handleInput(sen, s, rGroup, updates);
break;
case "2102": // Input_0: EV, inputEvent, S/SS/SSS/L
case "2202": // Input_1: EV, inputEvent
case "2302": // Input_2: EV, inputEvent
case "2402": // Input_3: EV, inputEvent
handleInputEvent(sen, getString(s.valueStr), -1, updates);
break;
case "2103": // EVC, inputEventCnt, U16
case "2203": // EVC, inputEventCnt, U16
case "2303": // EVC, inputEventCnt, U16
case "2403": // EVC, inputEventCnt, U16
handleInputEvent(sen, "", getInteger((int) s.value), updates);
break;
case "3101": // sensor_0: T, extTemp, C, -55/125; unknown 999
case "3201": // sensor_1: T, extTemp, C, -55/125; unknown 999
case "3301": // sensor_2: T, extTemp, C, -55/125; unknown 999
int idx = getExtTempId(sen.id);
if (idx >= 0) {
// H&T, Fllod, DW only have 1 channel, 1/1PM with Addon have up to to 3 sensors
String channel = profile.isSensor ? CHANNEL_SENSOR_TEMP : CHANNEL_SENSOR_TEMP + idx;
updateChannel(updates, CHANNEL_GROUP_SENSOR, channel,
toQuantityType(value, DIGITS_TEMP, SIUnits.CELSIUS));
} else {
logger.debug("{}: Unable to get extSensorId {} from {}/{}", thingName, sen.id, sen.type, sen.desc);
}
break;
case "3104": // T, deviceTemp, Celsius -40/300; 999=unknown
updateChannel(updates, CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_ITEMP,
toQuantityType(value, DIGITS_NONE, SIUnits.CELSIUS));
break;
case "3102": // sensor_0: T, extTemp, F, -67/257, unknown 999
case "3202": // sensor_1: T, extTemp, F, -67/257, unknown 999
case "3302": // sensor_2: T, extTemp, F, -67/257, unknown 999
case "3105": // T, deviceTemp, Fahrenheit -40/572
// skip, we use only C
break;
case "3107": // C, Gas concentration, U16
updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_PPM, getDecimal(s.value));
break;
case "3108": // DW: S, dwIsOpened, 0/1, -1=unknown
if (value != -1) {
updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_CONTACT,
value != 0 ? OpenClosedType.OPEN : OpenClosedType.CLOSED);
} else {
logger.debug("{}: Sensor error reported, check device, battery and installation", thingName);
}
break;
case "3113": // S, sensorOp, warmup/normal/fault
updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_SSTATE, getStringType(s.valueStr));
break;
case "3114": // S, selfTest, not_completed/completed/running/pending
updateChannel(updates, CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_SELFTTEST, getStringType(s.valueStr));
break;
case "3117": // S, extInput, 0/1
handleInput(sen, s, rGroup, updates);
break;
case "4101": // relay_0: P, power, W
case "4201": // relay_1: P, power, W
case "4301": // relay_2: P, power, W
case "4401": // relay_3: P, power, W
case "4105": // emeter_0: P, power, W
case "4205": // emeter_1: P, power, W
case "4305": // emeter_2: P, power, W
case "4102": // roller_0: P, rollerPower, W, 0-2300, unknown -1
case "4202": // roller_1: P, rollerPower, W, 0-2300, unknown -1
updateChannel(updates, mGroup, CHANNEL_METER_CURRENTWATTS,
toQuantityType(s.value, DIGITS_WATT, SmartHomeUnits.WATT));
updateChannel(updates, mGroup, CHANNEL_LAST_UPDATE, getTimestamp());
break;
case "4103": // relay_0: E, energy, Wmin, U32
case "4203": // relay_1: E, energy, Wmin, U32
case "4303": // relay_2: E, energy, Wmin, U32
case "4403": // relay_3: E, energy, Wmin, U32
case "4104": // roller_0: E, rollerEnergy, Wmin, U32, -1
case "4204": // roller_0: E, rollerEnergy, Wmin, U32, -1
case "4106": // emeter_0: E, energy, Wh, U32
case "4206": // emeter_1: E, energy, Wh, U32
case "4306": // emeter_2: E, energy, Wh, U32
double total = profile.isEMeter ? s.value / 1000 : s.value / 60 / 1000;
updateChannel(updates, mGroup, CHANNEL_METER_TOTALKWH,
toQuantityType(total, DIGITS_KWH, SmartHomeUnits.KILOWATT_HOUR));
break;
case "4107": // emeter_0: E, energyReturned, Wh, U32, -1
case "4207": // emeter_1: E, energyReturned, Wh, U32, -1
case "4307": // emeter_2: E, energyReturned, Wh, U32, -1
updateChannel(updates, mGroup, CHANNEL_EMETER_TOTALRET,
toQuantityType(getDouble(s.value) / 1000, DIGITS_KWH, SmartHomeUnits.KILOWATT_HOUR));
break;
case "4108": // emeter_0: V, voltage, 0-265V, U32, -1
case "4208": // emeter_1: V, voltage, 0-265V, U32, -1
case "4308": // emeter_2: V, voltage, 0-265V, U32, -1
updateChannel(updates, mGroup, CHANNEL_EMETER_VOLTAGE,
toQuantityType(getDouble(s.value), DIGITS_VOLT, SmartHomeUnits.VOLT));
break;
case "4109": // emeter_0: A, current, 0/120A, -1
case "4209": // emeter_1: A, current, 0/120A, -1
case "4309": // emeter_2: A, current, 0/120A, -1
updateChannel(updates, rGroup, CHANNEL_EMETER_CURRENT,
toQuantityType(getDouble(s.value), DIGITS_VOLT, SmartHomeUnits.AMPERE));
break;
case "4110": // emeter_0: S, powerFactor, 0/1, -1
case "4210": // emeter_1: S, powerFactor, 0/1, -1
case "4310": // emeter_2: S, powerFactor, 0/1, -1
updateChannel(updates, rGroup, CHANNEL_EMETER_PFACTOR, getDecimal(s.value));
break;
case "6101": // A, overtemp, 0/1
if (s.value == 1) {
thingHandler.postEvent(ALARM_TYPE_OVERTEMP, true);
}
break;
case "6102": // relay_0: A, overpower, 0/1
case "6202": // relay_1: A, overpower, 0/1
case "6302": // relay_2: A, overpower, 0/1
case "6402": // relay_3: A, overpower, 0/1
if (s.value == 1) {
thingHandler.postEvent(ALARM_TYPE_OVERPOWER, true);
}
break;
case "6104": // relay_0: A, loadError, 0/1
case "6204": // relay_1: A, loadError, 0/1
case "6304": // relay_2: A, loadError, 0/1
case "6404": // relay_3: A, loadError, 0/1
if (s.value == 1) {
thingHandler.postEvent(ALARM_TYPE_LOADERR, true);
}
break;
case "6103": // roller_0: A, rollerStopReason, normal/safety_switch/obstacle/overpower
reason = getString(s.valueStr);
updateChannel(updates, CHANNEL_GROUP_ROL_CONTROL, CHANNEL_ROL_CONTROL_STOPR, getStringType(reason));
if (!reason.isEmpty() && !reason.equalsIgnoreCase(SHELLY_API_STOPR_NORMAL)) {
thingHandler.postEvent("ROLLER_" + reason.toUpperCase(), true);
}
case "6106": // A, flood, 0/1, -1
updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_FLOOD,
value == 1 ? OnOffType.ON : OnOffType.OFF);
break;
case "6108": // A, gas, none/mild/heavy/test or unknown
updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_ALARM_STATE, getStringType(s.valueStr));
break;
case "6110": // A, vibration, 0/1, -1=unknown
updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_VIBRATION,
value == 1 ? OnOffType.ON : OnOffType.OFF);
break;
case "9102": // EV, wakeupEvent, battery/button/periodic/poweron/sensor/ext_power, "unknown"=unknown
thingHandler.updateWakeupReason(s.valueArray);
break;
case "9103": // EVC, cfgChanged, U16
if ((lastCfgCount != -1) && (lastCfgCount != s.value)) {
thingHandler.requestUpdates(1, true); // refresh config
}
lastCfgCount = (int) s.value;
break;
default:
processed = false;
}
return processed;
}
@Override
public CoIotDescrSen fixDescription(CoIotDescrSen sen, Map<String, CoIotDescrBlk> blkMap) {
return sen;
}
}

View File

@@ -0,0 +1,570 @@
/**
* Copyright (c) 2010-2020 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.shelly.internal.coap;
import static org.openhab.binding.shelly.internal.ShellyBindingConstants.*;
import static org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO.*;
import static org.openhab.binding.shelly.internal.util.ShellyUtils.*;
import java.net.UnknownHostException;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import org.eclipse.californium.core.CoapClient;
import org.eclipse.californium.core.coap.CoAP.Code;
import org.eclipse.californium.core.coap.CoAP.ResponseCode;
import org.eclipse.californium.core.coap.CoAP.Type;
import org.eclipse.californium.core.coap.MessageObserverAdapter;
import org.eclipse.californium.core.coap.Option;
import org.eclipse.californium.core.coap.OptionNumberRegistry;
import org.eclipse.californium.core.coap.Request;
import org.eclipse.californium.core.coap.Response;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.shelly.internal.api.ShellyApiException;
import org.openhab.binding.shelly.internal.api.ShellyDeviceProfile;
import org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO.CoIotDescrBlk;
import org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO.CoIotDescrSen;
import org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO.CoIotDevDescrTypeAdapter;
import org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO.CoIotDevDescription;
import org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO.CoIotGenericSensorList;
import org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO.CoIotSensor;
import org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO.CoIotSensorTypeAdapter;
import org.openhab.binding.shelly.internal.config.ShellyThingConfiguration;
import org.openhab.binding.shelly.internal.handler.ShellyBaseHandler;
import org.openhab.core.types.State;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonSyntaxException;
/**
* The {@link ShellyCoapHandler} handles the CoIoT/CoAP registration and events.
*
* @author Markus Michels - Initial contribution
*/
@NonNullByDefault
public class ShellyCoapHandler implements ShellyCoapListener {
private static final byte[] EMPTY_BYTE = new byte[0];
private final Logger logger = LoggerFactory.getLogger(ShellyCoapHandler.class);
private final ShellyBaseHandler thingHandler;
private ShellyThingConfiguration config = new ShellyThingConfiguration();
private final GsonBuilder gsonBuilder = new GsonBuilder();
private final Gson gson;
private String thingName;
private boolean coiotBound = false;
private ShellyCoIoTInterface coiot;
private int coiotVers = -1;
private final ShellyCoapServer coapServer;
private @Nullable CoapClient statusClient;
private Request reqDescription = new Request(Code.GET, Type.CON);
private Request reqStatus = new Request(Code.GET, Type.CON);
private boolean discovering = false;
private int lastSerial = -1;
private String lastPayload = "";
private Map<String, CoIotDescrBlk> blkMap = new LinkedHashMap<>();
private Map<String, CoIotDescrSen> sensorMap = new LinkedHashMap<>();
private final ShellyDeviceProfile profile;
public ShellyCoapHandler(ShellyBaseHandler thingHandler, ShellyCoapServer coapServer) {
this.thingHandler = thingHandler;
this.thingName = thingHandler.thingName;
this.coapServer = coapServer;
this.coiot = new ShellyCoIoTVersion1(thingName, thingHandler, blkMap, sensorMap); // Default
gsonBuilder.registerTypeAdapter(CoIotDevDescription.class, new CoIotDevDescrTypeAdapter());
gsonBuilder.registerTypeAdapter(CoIotGenericSensorList.class, new CoIotSensorTypeAdapter());
gson = gsonBuilder.create();
profile = thingHandler.getProfile();
}
/**
* Initialize CoAP access, send discovery packet and start Status server
*
* @parm thingName Thing name derived from Thing Type/hostname
* @parm config ShellyThingConfiguration
* @thows ShellyApiException
*/
public synchronized void start(String thingName, ShellyThingConfiguration config) throws ShellyApiException {
try {
this.thingName = thingName;
this.config = config;
if (isStarted()) {
logger.trace("{}: CoAP Listener was already started", thingName);
stop();
}
logger.debug("{}: Starting CoAP Listener", thingName);
coapServer.start(config.localIp, this);
statusClient = new CoapClient(completeUrl(config.deviceIp, COLOIT_URI_DEVSTATUS))
.setTimeout((long) SHELLY_API_TIMEOUT_MS).useNONs().setEndpoint(coapServer.getEndpoint());
discover();
} catch (UnknownHostException e) {
logger.debug("{}: CoAP Exception", thingName, e);
throw new ShellyApiException("Unknown Host: " + config.deviceIp, e);
}
}
public boolean isStarted() {
return statusClient != null;
}
/**
* Process an inbound Response (or mapped Request): decode CoAP options. handle discovery result or status updates
*
* @param response The Response packet
*/
@Override
public void processResponse(@Nullable Response response) {
if (response == null) {
return; // other device instance
}
String ip = response.getSourceContext().getPeerAddress().toString();
if (!ip.contains(config.deviceIp)) {
return;
}
String payload = "";
String devId = "";
String uri = "";
// int validity = 0;
int serial = -1;
try {
if (logger.isDebugEnabled()) {
logger.debug("{}: CoIoT Message from {} (MID={}): {}", thingName,
response.getSourceContext().getPeerAddress(), response.getMID(), response.getPayloadString());
}
if (response.isCanceled() || response.isDuplicate() || response.isRejected()) {
logger.debug("{} ({}): Packet was canceled, rejected or is a duplicate -> discard", thingName, devId);
return;
}
if (response.getCode() == ResponseCode.CONTENT) {
payload = response.getPayloadString();
List<Option> options = response.getOptions().asSortedList();
int i = 0;
while (i < options.size()) {
Option opt = options.get(i);
switch (opt.getNumber()) {
case OptionNumberRegistry.URI_PATH:
uri = COLOIT_URI_BASE + opt.getStringValue();
break;
case COIOT_OPTION_GLOBAL_DEVID:
devId = opt.getStringValue();
String sVersion = substringAfterLast(devId, "#");
int iVersion = Integer.parseInt(sVersion);
if (coiotBound && (coiotVers != iVersion)) {
logger.debug(
"{}: CoIoT versopm has changed from {} to {}, maybe the firmware was upgraded",
thingName, coiotVers, iVersion);
thingHandler.reinitializeThing();
coiotBound = false;
}
if (!coiotBound) {
thingHandler.updateProperties(PROPERTY_COAP_VERSION, sVersion);
logger.debug("{}: CoIoT Version {} detected", thingName, iVersion);
if (iVersion == COIOT_VERSION_1) {
coiot = new ShellyCoIoTVersion1(thingName, thingHandler, blkMap, sensorMap);
} else if (iVersion == COIOT_VERSION_2) {
coiot = new ShellyCoIoTVersion2(thingName, thingHandler, blkMap, sensorMap);
} else {
logger.warn("{}: Unsupported CoAP version detected: {}", thingName, sVersion);
return;
}
coiotVers = iVersion;
coiotBound = true;
}
break;
case COIOT_OPTION_STATUS_VALIDITY:
// validity = o.getIntegerValue();
break;
case COIOT_OPTION_STATUS_SERIAL:
serial = opt.getIntegerValue();
break;
default:
logger.debug("{} ({}): COAP option {} with value {} skipped", thingName, devId,
opt.getNumber(), opt.getValue());
}
i++;
}
// If we received a CoAP message successful the thing must be online
thingHandler.setThingOnline();
// The device changes the serial on every update, receiving a message with the same serial is a
// duplicate, excep for battery devices! Those reset the serial every time when they wake-up
if ((serial == lastSerial) && payload.equals(lastPayload)
&& (!profile.hasBattery || ((serial & 0xFF) != 0))) {
logger.debug("{}: Serial {} was already processed, ignore update", thingName, serial);
return;
}
// fixed malformed JSON :-(
payload = fixJSON(payload);
if (uri.equalsIgnoreCase(COLOIT_URI_DEVDESC) || (uri.isEmpty() && payload.contains(COIOT_TAG_BLK))) {
handleDeviceDescription(devId, payload);
} else if (uri.equalsIgnoreCase(COLOIT_URI_DEVSTATUS)
|| (uri.isEmpty() && payload.contains(COIOT_TAG_GENERIC))) {
handleStatusUpdate(devId, payload, serial);
}
} else {
// error handling
logger.debug("{}: Unknown Response Code {} received, payload={}", thingName, response.getCode(),
response.getPayloadString());
}
if (!discovering) {
// Observe Status Updates
reqStatus = sendRequest(reqStatus, config.deviceIp, COLOIT_URI_DEVSTATUS, Type.NON);
discovering = true;
}
} catch (IllegalArgumentException | NullPointerException e) {
logger.debug("{}: Unable to process CoIoT Message for payload={}", thingName, payload, e);
resetSerial();
}
}
/**
* Process a CoIoT device description message. This includes definitions on device units (Relay0, Relay1, Sensors
* etc.) as well as a definition of sensors and actors. This information needs to be stored allowing to map ids from
* status updates to the device units and matching the correct thing channel.
*
* @param devId The device id reported in the CoIoT message.
* @param payload Device desciption in JSon format, example:
* {"blk":[{"I":0,"D":"Relay0"}],"sen":[{"I":112,"T":"Switch","R":"0/1","L":0}],"act":[{"I":211,"D":"Switch","L":0,"P":[{"I":2011,"D":"ToState","R":"0/1"}]}]}
*/
private void handleDeviceDescription(String devId, String payload) {
logger.debug("{}: CoIoT Device Description for {}: {}", thingName, devId, payload);
try {
boolean valid = true;
// Decode Json
CoIotDevDescription descr = gson.fromJson(payload, CoIotDevDescription.class);
for (int i = 0; i < descr.blk.size(); i++) {
CoIotDescrBlk blk = descr.blk.get(i);
logger.debug("{}: id={}: {}", thingName, blk.id, blk.desc);
if (!blkMap.containsKey(blk.id)) {
blkMap.put(blk.id, blk);
} else {
blkMap.replace(blk.id, blk);
}
if ((blk.type != null) && !blk.type.isEmpty()) {
// in fact it is a sen entry - that's vioaling the Spec
logger.trace("{}: fix: auto-create sensor definition for id {}/{}!", thingName, blk.id,
blk.desc);
CoIotDescrSen sen = new CoIotDescrSen();
sen.id = blk.id;
sen.desc = blk.desc;
sen.type = blk.type;
sen.range = blk.range;
sen.links = blk.links;
valid &= addSensor(sen);
}
}
// Save to thing properties
thingHandler.updateProperties(PROPERTY_COAP_DESCR, payload);
logger.debug("{}: Adding {} sensor definitions", thingName, descr.sen.size());
if (descr.sen != null) {
for (int i = 0; i < descr.sen.size(); i++) {
valid &= addSensor(descr.sen.get(i));
}
}
if (!valid) {
logger.debug(
"{}: Incompatible device description detected for CoIoT version {} (id length mismatch), discarding!",
thingName, coiot.getVersion());
thingHandler.updateProperties(PROPERTY_COAP_DESCR, "");
discover();
return;
}
} catch (JsonSyntaxException e) {
logger.warn("{}: Unable to parse CoAP Device Description! JSON={}", thingName, payload);
} catch (NullPointerException | IllegalArgumentException e) {
logger.warn("{}: Unable to parse CoAP Device Description! JSON={}", thingName, payload, e);
}
}
/**
* Add a new sensor to the sensor table
*
* @param sen CoIotDescrSen of the sensor
*/
private synchronized boolean addSensor(CoIotDescrSen sen) {
logger.debug("{}: id {}: {}, Type={}, Range={}, Links={}", thingName, sen.id, sen.desc, sen.type, sen.range,
sen.links);
// CoIoT version 2 changes from 3 digit IDs to 4 digit IDs
// We need to make sure that the persisted device description matches,
// otherwise the stored one is discarded and a new discovery is triggered
// This happens on firmware up/downgrades (version 1.8 brings CoIoT v2 with 4 digit IDs)
int vers = coiot.getVersion();
if (((vers == COIOT_VERSION_1) && (sen.id.length() > 3))
|| ((vers >= COIOT_VERSION_2) && (sen.id.length() < 4))) {
return false;
}
try {
CoIotDescrSen fixed = coiot.fixDescription(sen, blkMap);
if (!sensorMap.containsKey(fixed.id)) {
sensorMap.put(sen.id, fixed);
} else {
sensorMap.replace(sen.id, fixed);
}
} catch (NullPointerException | IllegalArgumentException e) { // depending on firmware release the CoAP device
// description is buggy
logger.debug("{}: Unable to decode sensor definition -> skip", thingName, e);
}
return true;
}
/**
* Process CoIoT status update message. If a status update is received, but the device description has not been
* received yet a GET is send to query device description.
*
* @param devId device id included in the status packet
* @param payload CoAP payload (Json format), example: {"G":[[0,112,0]]}
* @param serial Serial for this request. If this the the same as last serial
* the update was already sent and processed so this one gets
* ignored.
*/
private void handleStatusUpdate(String devId, String payload, int serial) {
logger.debug("{}: CoIoT Sensor data {} (serial={})", thingName, payload, serial);
if (blkMap.isEmpty()) {
// send discovery packet
resetSerial();
discover();
// try to uses description from last initialization
String savedDescr = thingHandler.getProperty(PROPERTY_COAP_DESCR);
if (savedDescr.isEmpty()) {
logger.debug("{}: Device description not yet received, trigger auto-initialization", thingName);
return;
}
// simulate received device description to create element table
logger.debug("{}: Device description for {} restored: {}", thingName, devId, savedDescr);
handleDeviceDescription(devId, savedDescr);
}
// Parse Json,
CoIotGenericSensorList list = gson.fromJson(fixJSON(payload), CoIotGenericSensorList.class);
if (list.generic == null) {
logger.debug("{}: Sensor list has invalid format! Payload: {}", devId, payload);
return;
}
List<CoIotSensor> sensorUpdates = list.generic;
Map<String, State> updates = new TreeMap<String, State>();
logger.debug("{}: {} CoAP sensor updates received", thingName, sensorUpdates.size());
int failed = 0;
for (int i = 0; i < sensorUpdates.size(); i++) {
try {
CoIotSensor s = sensorUpdates.get(i);
if (!sensorMap.containsKey(s.id)) {
logger.debug("{}: Invalid id in sensor description: {}, index {}", thingName, s.id, i);
failed++;
continue;
}
CoIotDescrSen sen = sensorMap.get(s.id);
// find matching sensor definition from device description, use the Link ID as index
sen = coiot.fixDescription(sen, blkMap);
if (!blkMap.containsKey(sen.links)) {
logger.debug("{}: Invalid CoAP description: sen.links({}", thingName, getString(sen.links));
continue;
}
if (!blkMap.containsKey(sen.links)) {
logger.debug("{}: Unable to find BLK for link {} from sen.id={}", thingName, sen.links, sen.id);
continue;
}
CoIotDescrBlk element = blkMap.get(sen.links);
logger.trace("{}: Sensor value[{}]: id={}, Value={} ({}, Type={}, Range={}, Link={}: {})", thingName,
i, s.id, getString(s.valueStr).isEmpty() ? s.value : s.valueStr, sen.desc, sen.type, sen.range,
sen.links, element.desc);
if (!coiot.handleStatusUpdate(sensorUpdates, sen, s, updates)) {
logger.debug("{}: CoIoT data for id {}, type {}/{} not processed, value={}; payload={}", thingName,
sen.id, sen.type, sen.desc, s.value, payload);
}
} catch (NullPointerException | IllegalArgumentException e) {
// even the processing of one value failed we continue with the next one (sometimes this is caused by
// buggy formats provided by the device
logger.debug("{}: Unable to process data from sensor[{}], devId={}, payload={}", thingName, i, devId,
payload, e);
}
}
if (!updates.isEmpty()) {
int updated = 0;
for (Map.Entry<String, State> u : updates.entrySet()) {
updated += thingHandler.updateChannel(u.getKey(), u.getValue(), false) ? 1 : 0;
}
if (updated > 0) {
logger.debug("{}: {} channels updated from CoIoT status, serial={}", thingName, updated, serial);
if (profile.isSensor || profile.isRoller) {
// CoAP is currently lacking the lastUpdate info, so we use host timestamp
thingHandler.updateChannel(profile.getControlGroup(0), CHANNEL_LAST_UPDATE, getTimestamp());
}
}
// Old firmware release are lacking various status values, which are not updated using CoIoT.
// In this case we keep a refresh so it gets polled using REST. Beginning with Firmware 1.6 most
// of the values are available
if ((!thingHandler.autoCoIoT && (thingHandler.scheduledUpdates <= 1))
|| (thingHandler.autoCoIoT && !profile.isLight && !profile.hasBattery)) {
thingHandler.requestUpdates(1, false);
}
} else {
if (failed == sensorUpdates.size()) {
logger.debug("{}: Device description problem detected, re-discover", thingName);
coiotBound = false;
discover();
}
}
// Remember serial, new packets with same serial will be ignored
lastSerial = serial;
lastPayload = payload;
}
private void discover() {
reqDescription = sendRequest(reqDescription, config.deviceIp, COLOIT_URI_DEVDESC, Type.CON);
}
/**
* Fix malformed JSON - stupid, but the devices sometimes return malformed JSON with then causes a
* JsonSyntaxException
*
* @param json to be checked/fixed
*/
private static String fixJSON(String payload) {
String json = payload;
json = json.replace("}{", "},{");
json = json.replace("][", "],[");
json = json.replace("],,[", "],[");
return json;
}
/**
* Send a new request (Discovery to get Device Description). Before a pending
* request will be canceled.
*
* @param request The current request (this will be canceled an a new one will
* be created)
* @param ipAddress Device's IP address
* @param uri The URI we are calling (CoIoT = /cit/d or /cit/s)
* @param con true: send as CON, false: send as NON
* @return new packet
*/
private Request sendRequest(@Nullable Request request, String ipAddress, String uri, Type con) {
if ((request != null) && !request.isCanceled()) {
request.cancel();
}
resetSerial();
return newRequest(ipAddress, uri, con).send();
}
/**
* Allocate a new Request structure. A message observer will be added to get the
* callback when a response has been received.
*
* @param ipAddress IP address of the device
* @param uri URI to be addressed
* @param uri The URI we are calling (CoIoT = /cit/d or /cit/s)
* @param con true: send as CON, false: send as NON
* @return new packet
*/
private Request newRequest(String ipAddress, String uri, Type con) {
// We need to build our own Request to set an empty Token
Request request = new Request(Code.GET, con);
request.setURI(completeUrl(ipAddress, uri));
request.setToken(EMPTY_BYTE);
request.addMessageObserver(new MessageObserverAdapter() {
@Override
public void onResponse(@Nullable Response response) {
processResponse(response);
}
@Override
public void onCancel() {
logger.debug("{}: CoAP Request was canceled", thingName);
}
@Override
public void onTimeout() {
logger.debug("{}: CoAP Request timed out", thingName);
}
});
return request;
}
/**
* Reset serial and payload used to detect duplicate messages, which have to be ignored.
* We can't rely that the device manages serials correctly all the time. There are firmware releases sending updated
* sensor information with the serial from the last packet, which is wrong. We bypass this problem by comparing also
* the payload.
*/
private void resetSerial() {
lastSerial = -1;
lastPayload = "";
}
public int getVersion() {
return coiotVers;
}
/**
* Cancel pending requests and shutdown the client
*/
public synchronized void stop() {
if (isStarted()) {
logger.debug("{}: Stopping CoAP Listener", thingName);
coapServer.stop(this);
if (statusClient != null) {
statusClient.shutdown();
statusClient = null;
}
if (!reqDescription.isCanceled()) {
reqDescription.cancel();
}
if (!reqStatus.isCanceled()) {
reqStatus.cancel();
}
}
resetSerial();
coiotBound = false;
}
public void dispose() {
stop();
}
private static String completeUrl(String ipAddress, String uri) {
return "coap://" + ipAddress + ":" + COIOT_PORT + uri;
}
}

View File

@@ -0,0 +1,331 @@
/**
* Copyright (c) 2010-2020 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.shelly.internal.coap;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import com.google.gson.TypeAdapter;
import com.google.gson.annotations.SerializedName;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken;
import com.google.gson.stream.JsonWriter;
/**
* The {@link ShellyCoapJSonDTO} helps the CoIoT Json into Java objects
*
* @author Markus Michels - Initial contribution
*/
public class ShellyCoapJSonDTO {
// Coap
public static final int COIOT_VERSION_1 = 1;
public static final int COIOT_VERSION_2 = 2;
public static final int COIOT_PORT = 5683;
public static final String COAP_MULTICAST_ADDRESS = "224.0.1.187";
public static final String COLOIT_URI_BASE = "/cit/";
public static final String COLOIT_URI_DEVDESC = COLOIT_URI_BASE + "d";
public static final String COLOIT_URI_DEVSTATUS = COLOIT_URI_BASE + "s";
public static final int COIOT_OPTION_GLOBAL_DEVID = 3332;
public static final int COIOT_OPTION_STATUS_VALIDITY = 3412;
public static final int COIOT_OPTION_STATUS_SERIAL = 3420;
public static final String COIOT_TAG_BLK = "blk";
public static final String COIOT_TAG_SEN = "sen";
public static final String COIOT_TAG_ACT = "act";
public static final String COIOT_TAG_GENERIC = "G";
public static class CoIotDescrBlk {
@SerializedName("I")
String id; // ID
@SerializedName("D")
String desc; // Description
// Sometimes sen entries are part of the blk array - not conforming the Spec!
@SerializedName("T")
public String type; // Type
@SerializedName("R")
public String range; // Range
@SerializedName("L")
public String links; // Links
}
public static class CoIotDescrSen {
@SerializedName("I")
String id; // ID
@SerializedName("D")
String desc; // Description
@SerializedName("T")
public String type; // Type
@SerializedName("R")
public String range; // Range
@SerializedName("L")
public String links; // Links
@SerializedName("U")
public String unit; // Unit
}
public static class CoIotDescrP {
@SerializedName("I")
String id; // ID
@SerializedName("D")
String desc; // Description
@SerializedName("R")
public String range; // Range
}
public static class CoIotDescrAct {
@SerializedName("I")
String id; // ID
@SerializedName("D")
String desc; // Description
@SerializedName("L")
public String links; // Links
@SerializedName("P")
public List<CoIotDescrP> pTag; // ?
}
public static class CoIotDevDescription {
public List<CoIotDescrBlk> blk;
public List<CoIotDescrSen> sen;
// public List<CoIotDescrAct> act;
public CoIotDevDescription() {
blk = new ArrayList<>();
sen = new ArrayList<>();
}
}
public static class CoIotSensor {
@SerializedName("index")
public String id; // id
public double value; // value
public String valueStr; // value
public List<Object> valueArray;
}
public static class CoIotGenericSensorList {
@SerializedName("G")
public List<CoIotSensor> generic;
public CoIotGenericSensorList() {
generic = new ArrayList<>();
}
}
protected static class CoIotDevDescrTypeAdapter extends TypeAdapter<CoIotDevDescription> {
@Override
public CoIotDevDescription read(final JsonReader in) throws IOException {
CoIotDevDescription descr = new CoIotDevDescription();
/*
* parse JSON like
* "blk": [
* { "I": 0, "D": "Relay0"},
* { "I": 1, "D": "Sensors"} ],
* "sen": [
* { "I": 111, "T": "P", "D": "Power","R": "0/3500","L": 0},
* { "I": 112,"T": "S","D": "Switch","R": "0/1","L": 0}
* ]
*/
in.beginObject();
String name = in.nextName();
if (name.equalsIgnoreCase(COIOT_TAG_BLK)) {
in.beginArray();
while (in.hasNext()) {
CoIotDescrBlk blk = new CoIotDescrBlk();
in.beginObject();
while (in.hasNext()) {
switch (in.nextName().toUpperCase()) {
case "I":
blk.id = in.nextString();
break;
case "D":
blk.desc = in.nextString();
break;
default:
// skip data
in.nextNull();
}
}
in.endObject();
descr.blk.add(blk);
}
in.endArray();
name = in.nextName();
}
if (name.equalsIgnoreCase(COIOT_TAG_SEN)) {
/*
* parse sensor list, e.g.
* "sen":[
* { "I":111,"T":"Red","R":"0/255","L":0},
* { "I":121,"T":"Green","R":"0/255","L":0},
* ]
*/
in.beginArray();
while (in.hasNext()) {
CoIotDescrSen sen = new CoIotDescrSen();
in.beginObject();
while (in.hasNext()) {
String tag = in.nextName();
switch (tag.toUpperCase()) {
case "I":
sen.id = in.nextString();
break;
case "D":
sen.desc = in.nextString();
break;
case "T":
sen.type = in.nextString();
break;
case "R":
JsonToken token = in.peek();
if (token == JsonToken.BEGIN_ARRAY) {
// must be v2: an array
in.beginArray();
sen.range = "";
while (in.hasNext()) {
String value = in.nextString();
sen.range += sen.range.isEmpty() ? value : ";" + value;
}
in.endArray();
} else {
sen.range = in.nextString();
}
break;
case "L":
sen.links = String.valueOf(in.nextInt());
break;
case "U": // New in CoAPv2: unit"
sen.unit = in.nextString();
break;
default:
// skip data
in.nextNull();
}
}
in.endObject();
descr.sen.add(sen);
}
in.endArray();
}
in.endObject();
return descr;
}
@Override
public void write(final JsonWriter out, final CoIotDevDescription descr) throws IOException {
out.beginObject();
if (descr != null) {
out.name(COIOT_TAG_BLK).beginArray();
for (int i = 0; i < descr.blk.size(); i++) {
CoIotDescrBlk blk = descr.blk.get(i);
out.beginArray();
out.value(blk.id);
out.value(blk.desc);
out.endArray();
}
out.endArray();
out.name(COIOT_TAG_SEN).beginArray();
for (int i = 0; i < descr.sen.size(); i++) {
// Create element, e.g. {“I”:66, “D”:“lux”, “T”:“L”, “R”:“0/100000”, “L”:1},
CoIotDescrSen sen = descr.sen.get(i);
out.beginArray();
out.value(sen.id);
out.value(sen.desc);
out.value(sen.type);
out.value(sen.range);
out.value(sen.links);
if (sen.unit != null) {
out.value(sen.unit);
}
out.endArray();
}
out.endArray();
}
out.endObject();
}
}
protected static class CoIotSensorTypeAdapter extends TypeAdapter<CoIotGenericSensorList> {
@Override
public CoIotGenericSensorList read(final JsonReader in) throws IOException {
CoIotGenericSensorList list = new CoIotGenericSensorList();
in.beginObject();
String generic = in.nextName();
if (generic.equals(COIOT_TAG_GENERIC)) {
in.beginArray();
while (in.hasNext()) {
CoIotSensor sensor = new CoIotSensor();
in.beginArray();
in.nextInt(); // alway 0
sensor.id = Integer.toString(in.nextInt());
JsonToken token = in.peek();
if (token == JsonToken.STRING) {
// handle as string
sensor.valueStr = in.nextString();
sensor.value = -1;
} else if (token == JsonToken.NUMBER) {
// handle as double
sensor.value = in.nextDouble();
sensor.valueStr = "";
} else if (token == JsonToken.BEGIN_ARRAY) {
sensor.valueArray = new ArrayList<>();
in.beginArray();
while (in.hasNext()) {
if (in.peek() == JsonToken.STRING) {
sensor.valueArray.add(in.nextString());
} else {
// skip
in.nextNull();
}
}
in.endArray();
}
in.endArray();
list.generic.add(sensor);
}
in.endArray();
}
in.endObject();
return list;
}
@Override
public void write(final JsonWriter out, final CoIotGenericSensorList o) throws IOException {
CoIotGenericSensorList sensors = o;
out.beginObject();
if (sensors != null) {
out.name(COIOT_TAG_GENERIC).beginArray();
for (int i = 0; i < sensors.generic.size(); i++) {
out.beginArray();
out.value(0);
out.value(sensors.generic.get(i).id);
out.value(sensors.generic.get(i).value);
out.endArray();
}
out.endArray();
}
out.endObject();
}
}
}

View File

@@ -0,0 +1,27 @@
/**
* Copyright (c) 2010-2020 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.shelly.internal.coap;
import org.eclipse.californium.core.coap.Response;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* The {@link ShellyCoapListener} describes the listening interface to process Coap responses
*
* @author Markus Michels - Initial contribution
*/
@NonNullByDefault
public interface ShellyCoapListener {
public void processResponse(@Nullable Response response);
}

View File

@@ -0,0 +1,145 @@
/**
* Copyright (c) 2010-2020 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.shelly.internal.coap;
import static org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO.COIOT_PORT;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.UnknownHostException;
import java.util.Set;
import org.eclipse.californium.core.CoapResource;
import org.eclipse.californium.core.CoapServer;
import org.eclipse.californium.core.coap.CoAP;
import org.eclipse.californium.core.coap.CoAP.Code;
import org.eclipse.californium.core.coap.CoAP.ResponseCode;
import org.eclipse.californium.core.coap.Request;
import org.eclipse.californium.core.coap.Response;
import org.eclipse.californium.core.network.CoapEndpoint;
import org.eclipse.californium.core.network.Exchange;
import org.eclipse.californium.core.network.config.NetworkConfig;
import org.eclipse.californium.elements.UdpMulticastConnector;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.util.ConcurrentHashSet;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link ShellyCoapServer} implements the UDP listener and status event processor (for /cit/s messages)
*
* @author Markus Michels - Initial contribution
*/
@NonNullByDefault
public class ShellyCoapServer {
private final Logger logger = LoggerFactory.getLogger(ShellyCoapServer.class);
boolean started = false;
private CoapEndpoint statusEndpoint = new CoapEndpoint.Builder().build();
private @Nullable UdpMulticastConnector statusConnector;
private final CoapServer server = new CoapServer(NetworkConfig.getStandard(), COIOT_PORT);;
private final Set<ShellyCoapListener> coapListeners = new ConcurrentHashSet<>();
protected class ShellyStatusListener extends CoapResource {
private ShellyCoapServer listener;
public ShellyStatusListener(String uri, ShellyCoapServer listener) {
super(uri, true);
getAttributes().setTitle("ShellyCoapListener");
this.listener = listener;
}
@Override
public void handleRequest(@Nullable final Exchange exchange) {
if (exchange != null) {
Request request = exchange.getRequest();
Code code = exchange.getRequest().getCode();
switch (code) {
case CUSTOM_30:
listener.processResponse(createResponse(request));
break;
default:
super.handleRequest(exchange);
}
}
}
}
public synchronized void start(String localIp, ShellyCoapListener listener) throws UnknownHostException {
if (!started) {
logger.debug("Initializing CoIoT listener (local IP={}:{})", localIp, COIOT_PORT);
NetworkConfig nc = NetworkConfig.getStandard();
InetAddress localAddr = InetAddress.getByName(localIp);
InetSocketAddress localPort = new InetSocketAddress(COIOT_PORT);
// Join the multicast group on the selected network interface
statusConnector = new UdpMulticastConnector(localAddr, localPort, CoAP.MULTICAST_IPV4); // bind UDP listener
statusEndpoint = new CoapEndpoint.Builder().setNetworkConfig(nc).setConnector(statusConnector).build();
server.addEndpoint(statusEndpoint);
CoapResource cit = new ShellyStatusListener("cit", this);
CoapResource s = new ShellyStatusListener("s", this);
cit.add(s);
server.add(cit);
started = true;
}
if (!coapListeners.contains(listener)) {
coapListeners.add(listener);
}
}
protected void processResponse(Response response) {
coapListeners.forEach(listener -> listener.processResponse(response));
}
public static Response createResponse(Request request) {
Response response = Response.createResponse(request, ResponseCode.CONTENT);
response.setType(request.getType());
response.setSourceContext(request.getSourceContext());
response.setMID(request.getMID());
response.setOptions(request.getOptions());
response.setPayload(request.getPayload());
return response;
}
@Nullable
public CoapEndpoint getEndpoint() {
return statusEndpoint;
}
/**
* Cancel pending requests and shutdown the client
*/
public void stop(ShellyCoapListener listener) {
coapListeners.remove(listener);
if (coapListeners.isEmpty()) {
stop();
}
}
private synchronized void stop() {
if (started) {
// Last listener
server.stop();
statusEndpoint.stop();
coapListeners.clear();
started = false;
logger.debug("CoAP Listener stopped");
}
}
public void dispose() {
stop();
}
}

View File

@@ -0,0 +1,65 @@
/**
* Copyright (c) 2010-2020 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.shelly.internal.config;
import java.util.Collections;
import java.util.Dictionary;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link ShellyBindingConfiguration} class contains fields mapping binding configuration parameters.
*
* @author Markus Michels - Initial contribution
*/
@NonNullByDefault
public class ShellyBindingConfiguration {
// Binding Configuration Properties
public static final String CONFIG_DEF_HTTP_USER = "defaultUserId";
public static final String CONFIG_DEF_HTTP_PWD = "defaultPassword";
public static final String CONFIG_AUTOCOIOT = "autoCoIoT";
public String defaultUserId = ""; // default for http basic user id
public String defaultPassword = ""; // default for http basic auth password
public boolean autoCoIoT = true;
public void updateFromProperties(Map<String, Object> properties) {
for (Map.Entry<String, Object> e : properties.entrySet()) {
if (e.getValue() == null) {
continue;
}
switch (e.getKey()) {
case CONFIG_DEF_HTTP_USER:
defaultUserId = (String) e.getValue();
break;
case CONFIG_DEF_HTTP_PWD:
defaultPassword = (String) e.getValue();
break;
case CONFIG_AUTOCOIOT:
autoCoIoT = (boolean) e.getValue();
break;
}
}
}
public void updateFromProperties(Dictionary<String, Object> properties) {
List<String> keys = Collections.list(properties.keys());
Map<String, Object> dictCopy = keys.stream().collect(Collectors.toMap(Function.identity(), properties::get));
updateFromProperties(dictCopy);
}
}

View File

@@ -0,0 +1,41 @@
/**
* Copyright (c) 2010-2020 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.shelly.internal.config;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link ShellyThingConfiguration} class contains fields mapping thing configuration parameters.
*
* @author Markus Michels - Initial contribution
*/
@NonNullByDefault
public class ShellyThingConfiguration {
public String deviceIp = ""; // ip address of thedevice
public String userId = ""; // userid for http basic auth
public String password = ""; // password for http basic auth
public int updateInterval = 60; // schedule interval for the update job
public int lowBattery = 15; // threshold for battery value
public boolean brightnessAutoOn = true; // true: turn on device if brightness > 0 is set
public boolean eventsButton = false; // true: register for Relay btn_xxx events
public boolean eventsSwitch = true; // true: register for device out_xxx events
public boolean eventsPush = true; // true: register for short/long push events
public boolean eventsRoller = true; // true: register for short/long push events
public boolean eventsSensorReport = true; // true: register for sensor events
public boolean eventsCoIoT = false; // true: use CoIoT events (based on COAP)
public String localIp = ""; // local ip addresses used to create callback url
public String localPort = "8080";
}

View File

@@ -0,0 +1,220 @@
/**
* Copyright (c) 2010-2020 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.shelly.internal.discovery;
import static org.openhab.binding.shelly.internal.ShellyBindingConstants.*;
import static org.openhab.binding.shelly.internal.util.ShellyUtils.*;
import static org.openhab.core.thing.Thing.PROPERTY_MODEL_ID;
import java.io.IOException;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import javax.jmdns.ServiceInfo;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.openhab.binding.shelly.internal.api.ShellyApiException;
import org.openhab.binding.shelly.internal.api.ShellyApiResult;
import org.openhab.binding.shelly.internal.api.ShellyDeviceProfile;
import org.openhab.binding.shelly.internal.api.ShellyHttpApi;
import org.openhab.binding.shelly.internal.config.ShellyBindingConfiguration;
import org.openhab.binding.shelly.internal.config.ShellyThingConfiguration;
import org.openhab.binding.shelly.internal.handler.ShellyBaseHandler;
import org.openhab.binding.shelly.internal.util.ShellyTranslationProvider;
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.i18n.LocaleProvider;
import org.openhab.core.i18n.TranslationProvider;
import org.openhab.core.io.net.http.HttpClientFactory;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.ThingUID;
import org.osgi.service.cm.Configuration;
import org.osgi.service.cm.ConfigurationAdmin;
import org.osgi.service.component.ComponentContext;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Modified;
import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This class identifies Shelly devices by their mDNS service information.
*
* @author Markus Michels - Initial contribution
*/
@NonNullByDefault
@Component(service = MDNSDiscoveryParticipant.class, immediate = true)
public class ShellyDiscoveryParticipant implements MDNSDiscoveryParticipant {
private final Logger logger = LoggerFactory.getLogger(ShellyDiscoveryParticipant.class);
private final ShellyBindingConfiguration bindingConfig = new ShellyBindingConfiguration();
private final ShellyTranslationProvider messages;
private final HttpClient httpClient;
private final ConfigurationAdmin configurationAdmin;
/**
* OSGI Service Activation
*
* @param componentContext
* @param localeProvider
*/
@Activate
public ShellyDiscoveryParticipant(@Reference ConfigurationAdmin configurationAdmin,
@Reference HttpClientFactory httpClientFactory, @Reference LocaleProvider localeProvider,
@Reference TranslationProvider i18nProvider, ComponentContext componentContext) {
logger.debug("Activating ShellyDiscovery service");
this.configurationAdmin = configurationAdmin;
this.messages = new ShellyTranslationProvider(componentContext.getBundleContext().getBundle(), i18nProvider,
localeProvider);
this.httpClient = httpClientFactory.getCommonHttpClient();
bindingConfig.updateFromProperties(componentContext.getProperties());
}
@Override
public Set<ThingTypeUID> getSupportedThingTypeUIDs() {
return SUPPORTED_THING_TYPES_UIDS;
}
@Override
public String getServiceType() {
return SERVICE_TYPE;
}
/**
* Process updates to Binding Config
*
* @param componentContext
*/
@Modified
protected void modified(final ComponentContext componentContext) {
logger.debug("Shelly Binding Configuration refreshed");
bindingConfig.updateFromProperties(componentContext.getProperties());
}
@Nullable
@Override
public DiscoveryResult createResult(final ServiceInfo service) {
String name = service.getName().toLowerCase(); // Duao: Name starts with" Shelly" rather than "shelly"
if (!name.startsWith("shelly")) {
return null;
}
String address = "";
try {
String mode = "";
String model = "unknown";
String deviceName = "";
ThingUID thingUID = null;
ShellyDeviceProfile profile = null;
Map<String, Object> properties = new TreeMap<>();
name = service.getName().toLowerCase();
address = service.getHostAddress();
if ((address == null) || address.isEmpty()) {
logger.trace("{}: Shelly device discovered with empty IP address (service-name={})", name, service);
return null;
}
String thingType = service.getQualifiedName().contains(SERVICE_TYPE) && name.contains("-")
? substringBeforeLast(name, "-")
: name;
logger.debug("{}: Shelly device discovered: IP-Adress={}, type={}", name, address, thingType);
// Get device settings
Configuration serviceConfig = configurationAdmin.getConfiguration("binding.shelly");
if (serviceConfig.getProperties() != null) {
bindingConfig.updateFromProperties(serviceConfig.getProperties());
}
ShellyThingConfiguration config = new ShellyThingConfiguration();
config.deviceIp = address;
config.userId = bindingConfig.defaultUserId;
config.password = bindingConfig.defaultPassword;
try {
ShellyHttpApi api = new ShellyHttpApi(name, config, httpClient);
profile = api.getDeviceProfile(thingType);
logger.debug("{}: Shelly settings : {}", name, profile.settingsJson);
deviceName = getString(profile.settings.name);
model = getString(profile.settings.device.type);
mode = profile.mode;
properties = ShellyBaseHandler.fillDeviceProperties(profile);
logger.trace("{}: thingType={}, deviceType={}, mode={}, symbolic name={}", name, thingType,
profile.deviceType, mode.isEmpty() ? "<standard>" : mode, deviceName);
// get thing type from device name
thingUID = ShellyThingCreator.getThingUID(name, model, mode, false);
} catch (ShellyApiException e) {
ShellyApiResult result = e.getApiResult();
if (result.isHttpAccessUnauthorized()) {
logger.info("{}: {}", name, messages.get("discovery.protected", address));
// create shellyunknown thing - will be changed during thing initialization with valid credentials
thingUID = ShellyThingCreator.getThingUID(name, model, mode, true);
} else {
logger.info("{}: {}", name, messages.get("discovery.failed", address, e.toString()));
logger.debug("{}: Discovery failed", name, e);
}
} catch (IllegalArgumentException e) { // maybe some format description was buggy
logger.debug("{}: Discovery failed!", name, e);
}
if (thingUID != null) {
addProperty(properties, CONFIG_DEVICEIP, address);
addProperty(properties, PROPERTY_MODEL_ID, model);
addProperty(properties, PROPERTY_SERVICE_NAME, name);
addProperty(properties, PROPERTY_DEV_NAME, deviceName);
addProperty(properties, PROPERTY_DEV_TYPE, thingType);
addProperty(properties, PROPERTY_DEV_MODE, mode);
logger.debug("{}: Adding Shelly {}, UID={}", name, deviceName, thingUID.getAsString());
String thingLabel = deviceName.isEmpty() ? name + " - " + address
: deviceName + " (" + name + "@" + address + ")";
return DiscoveryResultBuilder.create(thingUID).withProperties(properties).withLabel(thingLabel)
.withRepresentationProperty(name).build();
}
} catch (IOException | NullPointerException e) {
// maybe some format description was buggy
logger.debug("{}: Exception on processing serviceInfo '{}'", name, service.getNiceTextString(), e);
}
return null;
}
private void addProperty(Map<String, Object> properties, String key, @Nullable String value) {
properties.put(key, value != null ? value : "");
}
@Nullable
@Override
public ThingUID getThingUID(@Nullable ServiceInfo service) throws IllegalArgumentException {
logger.debug("ServiceInfo {}", service);
if (service == null) {
throw new IllegalArgumentException("service must not be null!");
}
String serviceName = service.getName();
if (serviceName == null) {
throw new IllegalArgumentException("serviceName must not be null!");
}
serviceName = serviceName.toLowerCase();
if (!serviceName.contains(VENDOR.toLowerCase())) {
logger.debug("Not a " + VENDOR + " device!");
return null;
}
return ShellyThingCreator.getThingUID(serviceName, "", "", false);
}
}

View File

@@ -0,0 +1,130 @@
/**
* Copyright (c) 2010-2020 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.shelly.internal.discovery;
import static org.openhab.binding.shelly.internal.ShellyBindingConstants.*;
import static org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.*;
import static org.openhab.binding.shelly.internal.util.ShellyUtils.*;
import java.util.LinkedHashMap;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.ThingUID;
/**
* The {@link ShellyThingCreator} maps the device id into the thing type id
*
* @author Markus Michels - Initial contribution
*/
@NonNullByDefault
public class ShellyThingCreator {
private static final Map<String, String> THING_TYPE_MAPPING = new LinkedHashMap<>();
static {
// mapping by thing type
THING_TYPE_MAPPING.put(SHELLYDT_1PM, THING_TYPE_SHELLY1PM_STR);
THING_TYPE_MAPPING.put(SHELLYDT_1, THING_TYPE_SHELLY1_STR);
THING_TYPE_MAPPING.put(SHELLYDT_3EM, THING_TYPE_SHELLY3EM_STR);
THING_TYPE_MAPPING.put(SHELLYDT_EM, THING_TYPE_SHELLYEM_STR);
THING_TYPE_MAPPING.put(SHELLYDT_GAS, THING_TYPE_SHELLYGAS_STR);
THING_TYPE_MAPPING.put(SHELLYDT_DW, THING_TYPE_SHELLYDOORWIN_STR);
THING_TYPE_MAPPING.put(SHELLYDT_DW2, THING_TYPE_SHELLYDOORWIN2_STR);
THING_TYPE_MAPPING.put(SHELLYDT_DUO, THING_TYPE_SHELLYDUO_STR);
THING_TYPE_MAPPING.put(SHELLYDT_BULB, THING_TYPE_SHELLYBULB_STR);
THING_TYPE_MAPPING.put(SHELLYDT_VINTAGE, THING_TYPE_SHELLYVINTAGE_STR);
THING_TYPE_MAPPING.put(SHELLYDT_DIMMER, THING_TYPE_SHELLYDIMMER_STR);
THING_TYPE_MAPPING.put(SHELLYDT_DIMMER2, THING_TYPE_SHELLYDIMMER2_STR);
THING_TYPE_MAPPING.put(SHELLYDT_IX3, THING_TYPE_SHELLYIX3_STR);
THING_TYPE_MAPPING.put(SHELLYDT_BUTTON1, THING_TYPE_SHELLYBUTTON1_STR);
// mapping by thing type
THING_TYPE_MAPPING.put(THING_TYPE_SHELLY1_STR, THING_TYPE_SHELLY1_STR);
THING_TYPE_MAPPING.put(THING_TYPE_SHELLY1PM_STR, THING_TYPE_SHELLY1PM_STR);
THING_TYPE_MAPPING.put(THING_TYPE_SHELLY4PRO_STR, THING_TYPE_SHELLY4PRO_STR);
THING_TYPE_MAPPING.put(THING_TYPE_SHELLYDIMMER2_STR, THING_TYPE_SHELLYDIMMER2_STR);
THING_TYPE_MAPPING.put(THING_TYPE_SHELLYDIMMER_STR, THING_TYPE_SHELLYDIMMER_STR);
THING_TYPE_MAPPING.put(THING_TYPE_SHELLYIX3_STR, THING_TYPE_SHELLYIX3_STR);
THING_TYPE_MAPPING.put(THING_TYPE_SHELLY3EM_STR, THING_TYPE_SHELLY3EM_STR);
THING_TYPE_MAPPING.put(THING_TYPE_SHELLYEM_STR, THING_TYPE_SHELLYEM_STR);
THING_TYPE_MAPPING.put(THING_TYPE_SHELLYDUO_STR, THING_TYPE_SHELLYDUO_STR);
THING_TYPE_MAPPING.put(THING_TYPE_SHELLYVINTAGE_STR, THING_TYPE_SHELLYVINTAGE_STR);
THING_TYPE_MAPPING.put(THING_TYPE_SHELLYBULB_STR, THING_TYPE_SHELLYBULB_STR);
THING_TYPE_MAPPING.put(THING_TYPE_SHELLYDUO_STR, THING_TYPE_SHELLYDUO_STR);
THING_TYPE_MAPPING.put(THING_TYPE_SHELLYHT_STR, THING_TYPE_SHELLYHT_STR);
THING_TYPE_MAPPING.put(THING_TYPE_SHELLYSMOKE_STR, THING_TYPE_SHELLYSMOKE_STR);
THING_TYPE_MAPPING.put(THING_TYPE_SHELLYGAS_STR, THING_TYPE_SHELLYGAS_STR);
THING_TYPE_MAPPING.put(THING_TYPE_SHELLYFLOOD_STR, THING_TYPE_SHELLYFLOOD_STR);
THING_TYPE_MAPPING.put(THING_TYPE_SHELLYDOORWIN_STR, THING_TYPE_SHELLYDOORWIN_STR);
THING_TYPE_MAPPING.put(THING_TYPE_SHELLYDOORWIN2_STR, THING_TYPE_SHELLYDOORWIN2_STR);
THING_TYPE_MAPPING.put(THING_TYPE_SHELLYSENSE_STR, THING_TYPE_SHELLYSENSE_STR);
THING_TYPE_MAPPING.put(THING_TYPE_SHELLYEYE_STR, THING_TYPE_SHELLYEYE_STR);
THING_TYPE_MAPPING.put(THING_TYPE_SHELLYBUTTON1_STR, THING_TYPE_SHELLYBUTTON1_STR);
THING_TYPE_MAPPING.put(THING_TYPE_SHELLYPROTECTED_STR, THING_TYPE_SHELLYPROTECTED_STR);
}
public static ThingUID getThingUID(String serviceName, String deviceType, String mode, boolean unknown) {
String devid = substringAfterLast(serviceName, "-");
if (devid.isEmpty()) {
throw new IllegalArgumentException("serviceName has improper format: " + serviceName);
}
return new ThingUID(!unknown ? getThingTypeUID(serviceName, deviceType, mode)
: getThingTypeUID(THING_TYPE_SHELLYPROTECTED_STR + "-" + devid, deviceType, mode), devid);
}
public static ThingTypeUID getThingTypeUID(String serviceName, String deviceType, String mode) {
return new ThingTypeUID(BINDING_ID, getThingType(serviceName, deviceType, mode));
}
public static ThingTypeUID getUnknownTTUID() {
return new ThingTypeUID(BINDING_ID, THING_TYPE_SHELLYPROTECTED_STR);
}
public static String getThingType(String hostname, String deviceType, String mode) {
String name = hostname.toLowerCase();
String type = substringBefore(name, "-").toLowerCase();
String devid = substringAfterLast(name, "-");
if ((devid == null) || (type == null)) {
throw new IllegalArgumentException("Invalid device name format: " + hostname);
}
// First check for special handling
if (name.startsWith(THING_TYPE_SHELLY25_PREFIX)) { // Shelly v2.5
return mode.equals(SHELLY_MODE_RELAY) ? THING_TYPE_SHELLY25_RELAY_STR : THING_TYPE_SHELLY25_ROLLER_STR;
}
if (name.startsWith(THING_TYPE_SHELLY2_PREFIX)) { // Shelly v2
return mode.equals(SHELLY_MODE_RELAY) ? THING_TYPE_SHELLY2_RELAY_STR : THING_TYPE_SHELLY2_ROLLER_STR;
}
if (name.startsWith(THING_TYPE_SHELLYPLUG_STR)) {
// shellyplug-s needs to be mapped to shellyplugs to follow the schema
// for the thing types: <thing type>-<mode>
if (name.startsWith(THING_TYPE_SHELLYPLUGS_STR) || name.contains("-s")) {
return THING_TYPE_SHELLYPLUGS_STR;
}
return THING_TYPE_SHELLYPLUG_STR;
}
if (name.startsWith(THING_TYPE_SHELLYRGBW2_PREFIX)) {
return mode.equals(SHELLY_MODE_COLOR) ? THING_TYPE_SHELLYRGBW2_COLOR_STR : THING_TYPE_SHELLYRGBW2_WHITE_STR;
}
// Check general mapping
if (!deviceType.isEmpty() && THING_TYPE_MAPPING.containsKey(deviceType)) {
return THING_TYPE_MAPPING.get(deviceType);
}
if (THING_TYPE_MAPPING.containsKey(type)) {
return THING_TYPE_MAPPING.get(type);
}
return THING_TYPE_SHELLYUNKNOWN_STR;
}
}

View File

@@ -0,0 +1,393 @@
/**
* Copyright (c) 2010-2020 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.shelly.internal.handler;
import static org.openhab.binding.shelly.internal.ShellyBindingConstants.*;
import static org.openhab.binding.shelly.internal.util.ShellyUtils.*;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellyControlRoller;
import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsEMeter;
import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsMeter;
import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsRelay;
import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsStatus;
import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellyStatusRelay;
import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellyStatusSensor;
import org.openhab.binding.shelly.internal.api.ShellyDeviceProfile;
import org.openhab.binding.shelly.internal.util.ShellyTranslationProvider;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.binding.builder.ChannelBuilder;
import org.openhab.core.thing.type.ChannelTypeUID;
/**
* The {@link ShellyCHANNEL_DEFINITIONSDTO} defines channel information for dynamically created channels. Those will be
* added on the first thing status update
*
* @author Markus Michels - Initial contribution
*/
@NonNullByDefault
public class ShellyChannelDefinitionsDTO {
private static final ChannelMap CHANNEL_DEFINITIONS = new ChannelMap();
// shortcuts to avoid line breaks (make code more readable)
private static final String CHGR_DEVST = CHANNEL_GROUP_DEV_STATUS;
private static final String CHGR_RELAY = CHANNEL_GROUP_RELAY_CONTROL;
private static final String CHGR_ROLLER = CHANNEL_GROUP_ROL_CONTROL;
private static final String CHGR_STATUS = CHANNEL_GROUP_STATUS;
private static final String CHGR_METER = CHANNEL_GROUP_METER;
private static final String CHGR_SENSOR = CHANNEL_GROUP_SENSOR;
private static final String CHGR_BAT = CHANNEL_GROUP_BATTERY;
public static final String ITEM_TYPE_NUMBER = "Number";
public static final String ITEM_TYPE_STRING = "String";
public static final String ITEM_TYPE_SWITCH = "Switch";
public static final String ITEM_TYPE_CONTACT = "Contact";
public static final String ITEM_TYPE_DATETIME = "DateTime";
public static final String ITEM_TYPE_TEMP = "Number:Temperature";
public static final String ITEM_TYPE_LUX = "Number:Illuminance";
public static final String ITEM_TYPE_POWER = "Number:Power";
public static final String ITEM_TYPE_ENERGY = "Number:Energy";
public static final String ITEM_TYPE_VOLT = "Number:ElectricPotential";
public static final String ITEM_TYPE_AMP = "Number:ElectricPotential";
public static final String ITEM_TYPE_PERCENT = "Number:Dimensionless";
public static final String ITEM_TYPE_ANGLE = "Number:Angle";
public static final String PREFIX_GROUP = "definitions.shelly.group.";
public static final String PREFIX_CHANNEL = "channel-type.shelly.";
public static final String SUFFIX_LABEL = ".label";
public static final String SUFFIX_DESCR = ".description";
public ShellyChannelDefinitionsDTO(ShellyTranslationProvider m) {
// Device: Internal Temp
CHANNEL_DEFINITIONS
// Device
.add(new ShellyChannel(m, CHGR_DEVST, CHANNEL_DEVST_NAME, "deviceName", ITEM_TYPE_STRING))
.add(new ShellyChannel(m, CHGR_DEVST, CHANNEL_DEVST_ITEMP, "deviceTemp", ITEM_TYPE_TEMP))
.add(new ShellyChannel(m, CHGR_DEVST, CHANNEL_DEVST_WAKEUP, "sensorWakeup", ITEM_TYPE_STRING))
.add(new ShellyChannel(m, CHGR_DEVST, CHANNEL_DEVST_ACCUWATTS, "meterAccuWatts", ITEM_TYPE_POWER))
.add(new ShellyChannel(m, CHGR_DEVST, CHANNEL_DEVST_ACCUTOTAL, "meterAccuTotal", ITEM_TYPE_POWER))
.add(new ShellyChannel(m, CHGR_DEVST, CHANNEL_DEVST_ACCURETURNED, "meterAccuReturned", ITEM_TYPE_POWER))
.add(new ShellyChannel(m, CHGR_DEVST, CHANNEL_DEVST_CHARGER, "charger", ITEM_TYPE_SWITCH))
.add(new ShellyChannel(m, CHGR_DEVST, CHANNEL_LED_STATUS_DISABLE, "ledStatusDisable", ITEM_TYPE_SWITCH))
.add(new ShellyChannel(m, CHGR_DEVST, CHANNEL_LED_POWER_DISABLE, "ledPowerDisable", ITEM_TYPE_SWITCH))
.add(new ShellyChannel(m, CHGR_DEVST, CHANNEL_DEVST_SELFTTEST, "selfTest", ITEM_TYPE_STRING))
.add(new ShellyChannel(m, CHGR_DEVST, CHANNEL_DEVST_UPTIME, "uptime", ITEM_TYPE_NUMBER))
.add(new ShellyChannel(m, CHGR_DEVST, CHANNEL_DEVST_HEARTBEAT, "heartBeat", ITEM_TYPE_DATETIME))
.add(new ShellyChannel(m, CHGR_DEVST, CHANNEL_DEVST_UPDATE, "updateAvailable", ITEM_TYPE_SWITCH))
// Relay
.add(new ShellyChannel(m, CHGR_RELAY, CHANNEL_OUTPUT_NAME, "outputName", ITEM_TYPE_STRING))
// Roller
.add(new ShellyChannel(m, CHGR_ROLLER, CHANNEL_ROL_CONTROL_STATE, "rollerState", ITEM_TYPE_STRING))
// RGBW2
.add(new ShellyChannel(m, CHANNEL_GROUP_LIGHT_CONTROL, CHANNEL_INPUT, "inputState", ITEM_TYPE_SWITCH))
// Power Meter
.add(new ShellyChannel(m, CHGR_METER, CHANNEL_METER_CURRENTWATTS, "meterWatts", ITEM_TYPE_POWER))
.add(new ShellyChannel(m, CHGR_METER, CHANNEL_METER_TOTALKWH, "meterTotal", ITEM_TYPE_ENERGY))
.add(new ShellyChannel(m, CHGR_METER, CHANNEL_METER_LASTMIN1, "lastPower1", ITEM_TYPE_ENERGY))
.add(new ShellyChannel(m, CHGR_METER, CHANNEL_LAST_UPDATE, "lastUpdate", ITEM_TYPE_DATETIME))
// EMeter
.add(new ShellyChannel(m, CHGR_METER, CHANNEL_EMETER_TOTALRET, "meterReturned", ITEM_TYPE_ENERGY))
.add(new ShellyChannel(m, CHGR_METER, CHANNEL_EMETER_REACTWATTS, "meterReactive", ITEM_TYPE_POWER))
.add(new ShellyChannel(m, CHGR_METER, CHANNEL_EMETER_VOLTAGE, "meterVoltage", ITEM_TYPE_VOLT))
.add(new ShellyChannel(m, CHGR_METER, CHANNEL_EMETER_CURRENT, "meterCurrent", ITEM_TYPE_AMP))
.add(new ShellyChannel(m, CHGR_METER, CHANNEL_EMETER_PFACTOR, "meterPowerFactor", ITEM_TYPE_NUMBER))
// Sensors
.add(new ShellyChannel(m, CHGR_SENSOR, CHANNEL_SENSOR_TEMP, "sensorTemp", ITEM_TYPE_TEMP))
.add(new ShellyChannel(m, CHGR_SENSOR, CHANNEL_SENSOR_HUM, "sensorHumidity", ITEM_TYPE_PERCENT))
.add(new ShellyChannel(m, CHGR_SENSOR, CHANNEL_SENSOR_LUX, "sensorLux", ITEM_TYPE_LUX))
.add(new ShellyChannel(m, CHGR_SENSOR, CHANNEL_SENSOR_ILLUM, "sensorIllumination", ITEM_TYPE_STRING))
.add(new ShellyChannel(m, CHGR_SENSOR, CHANNEL_SENSOR_CONTACT, "sensorContact", ITEM_TYPE_CONTACT))
.add(new ShellyChannel(m, CHGR_SENSOR, CHANNEL_SENSOR_SSTATE, "sensorState", ITEM_TYPE_STRING))
.add(new ShellyChannel(m, CHGR_SENSOR, CHANNEL_SENSOR_VIBRATION, "sensorVibration", ITEM_TYPE_SWITCH))
.add(new ShellyChannel(m, CHGR_SENSOR, CHANNEL_SENSOR_TILT, "sensorTilt", ITEM_TYPE_ANGLE))
.add(new ShellyChannel(m, CHGR_SENSOR, CHANNEL_SENSOR_MOTION, "sensorMotion", ITEM_TYPE_SWITCH))
.add(new ShellyChannel(m, CHGR_SENSOR, CHANNEL_SENSOR_FLOOD, "sensorFlood", ITEM_TYPE_SWITCH))
.add(new ShellyChannel(m, CHGR_SENSOR, CHANNEL_SENSOR_SMOKE, "sensorSmoke", ITEM_TYPE_SWITCH))
.add(new ShellyChannel(m, CHGR_SENSOR, CHANNEL_SENSOR_PPM, "sensorPPM", ITEM_TYPE_NUMBER))
.add(new ShellyChannel(m, CHGR_SENSOR, CHANNEL_SENSOR_VALVE, "sensorValve", ITEM_TYPE_STRING))
.add(new ShellyChannel(m, CHGR_SENSOR, CHANNEL_SENSOR_ALARM_STATE, "alarmState", ITEM_TYPE_STRING))
.add(new ShellyChannel(m, CHGR_SENSOR, CHANNEL_SENSOR_ERROR, "sensorError", ITEM_TYPE_STRING))
.add(new ShellyChannel(m, CHGR_SENSOR, CHANNEL_LAST_UPDATE, "lastUpdate", ITEM_TYPE_DATETIME))
// Button/ix3
.add(new ShellyChannel(m, CHGR_STATUS, CHANNEL_INPUT, "inputState", ITEM_TYPE_SWITCH))
.add(new ShellyChannel(m, CHGR_STATUS, CHANNEL_STATUS_EVENTTYPE, "eventType", ITEM_TYPE_STRING))
.add(new ShellyChannel(m, CHGR_STATUS, CHANNEL_STATUS_EVENTCOUNT, "eventCount", ITEM_TYPE_NUMBER))
.add(new ShellyChannel(m, CHGR_STATUS, CHANNEL_BUTTON_TRIGGER, "system.button", ITEM_TYPE_STRING))
.add(new ShellyChannel(m, CHGR_STATUS, CHANNEL_LAST_UPDATE, "lastUpdate", ITEM_TYPE_DATETIME))
// Addon with external sensors
.add(new ShellyChannel(m, CHGR_SENSOR, CHANNEL_ESENDOR_TEMP1, "sensorExtTemp", ITEM_TYPE_TEMP))
.add(new ShellyChannel(m, CHGR_SENSOR, CHANNEL_ESENDOR_TEMP2, "sensorExtTemp", ITEM_TYPE_TEMP))
.add(new ShellyChannel(m, CHGR_SENSOR, CHANNEL_ESENDOR_TEMP3, "sensorExtTemp", ITEM_TYPE_TEMP))
.add(new ShellyChannel(m, CHGR_SENSOR, CHANNEL_ESENDOR_HUMIDITY, "sensorExtHum", ITEM_TYPE_PERCENT))
// Battery
.add(new ShellyChannel(m, CHGR_BAT, CHANNEL_SENSOR_BAT_LEVEL, "system:battery-level",
ITEM_TYPE_PERCENT))
.add(new ShellyChannel(m, CHGR_BAT, CHANNEL_SENSOR_BAT_LOW, "system:low-battery", ITEM_TYPE_SWITCH));
}
public static ShellyChannel getDefinition(String channelName) throws IllegalArgumentException {
String group = substringBefore(channelName, "#");
String channel = substringAfter(channelName, "#");
if (group.contains(CHANNEL_GROUP_METER)) {
group = CHANNEL_GROUP_METER; // map meter1..n to meter
} else if (group.contains(CHANNEL_GROUP_RELAY_CONTROL)) {
group = CHANNEL_GROUP_RELAY_CONTROL; // map meter1..n to meter
} else if (group.contains(CHANNEL_GROUP_LIGHT_CHANNEL)) {
group = CHANNEL_GROUP_LIGHT_CHANNEL;
} else if (group.contains(CHANNEL_GROUP_STATUS)) {
group = CHANNEL_GROUP_STATUS; // map status1..n to meter
}
String channelId = group + "#" + channel;
return CHANNEL_DEFINITIONS.get(channelId);
}
/**
* Auto-create relay channels depending on relay type/mode
*
* @return ArrayList<Channel> of channels to be added to the thing
*/
public static Map<String, Channel> createDeviceChannels(final Thing thing, final ShellyDeviceProfile profile,
final ShellySettingsStatus status) {
Map<String, Channel> add = new LinkedHashMap<>();
addChannel(thing, add, profile.settings.name != null, CHGR_DEVST, CHANNEL_DEVST_NAME);
if (!profile.isSensor) {
// Only some devices report the internal device temp
addChannel(thing, add, (status.tmp != null) || (status.temperature != null), CHGR_DEVST,
CHANNEL_DEVST_ITEMP);
}
// RGBW2
addChannel(thing, add, status.input != null, CHANNEL_GROUP_LIGHT_CONTROL, CHANNEL_INPUT);
// If device has more than 1 meter the channel accumulatedWatts receives the accumulated value
boolean accuChannel = (((status.meters != null) && (status.meters.size() > 1) && !profile.isRoller
&& !profile.isRGBW2) || ((status.emeters != null && status.emeters.size() > 1)));
addChannel(thing, add, accuChannel, CHGR_DEVST, CHANNEL_DEVST_ACCUWATTS);
addChannel(thing, add, accuChannel, CHGR_DEVST, CHANNEL_DEVST_ACCUTOTAL);
addChannel(thing, add, accuChannel && (status.emeters != null), CHGR_DEVST, CHANNEL_DEVST_ACCURETURNED);
addChannel(thing, add, true, CHGR_DEVST, CHANNEL_DEVST_UPDATE);
addChannel(thing, add, true, CHGR_DEVST, CHANNEL_DEVST_UPTIME);
addChannel(thing, add, true, CHGR_DEVST, CHANNEL_DEVST_HEARTBEAT);
if (profile.settings.ledPowerDisable != null) {
addChannel(thing, add, true, CHGR_DEVST, CHANNEL_LED_POWER_DISABLE);
}
if (profile.settings.ledStatusDisable != null) {
addChannel(thing, add, true, CHGR_DEVST, CHANNEL_LED_STATUS_DISABLE); // WiFi status LED
}
return add;
}
/**
* Auto-create relay channels depending on relay type/mode
*
* @return ArrayList<Channel> of channels to be added to the thing
*/
public static Map<String, Channel> createRelayChannels(final Thing thing, final ShellyDeviceProfile profile,
final ShellyStatusRelay relay, int idx) {
Map<String, Channel> add = new LinkedHashMap<>();
String group = profile.getControlGroup(idx);
ShellySettingsRelay rs = profile.settings.relays.get(idx);
addChannel(thing, add, rs.name != null, group, CHANNEL_OUTPUT_NAME);
// Shelly 1/1PM Addon
if (relay.extTemperature != null) {
addChannel(thing, add, relay.extTemperature.sensor1 != null, CHGR_SENSOR, CHANNEL_ESENDOR_TEMP1);
addChannel(thing, add, relay.extTemperature.sensor2 != null, CHGR_SENSOR, CHANNEL_ESENDOR_TEMP2);
addChannel(thing, add, relay.extTemperature.sensor3 != null, CHGR_SENSOR, CHANNEL_ESENDOR_TEMP3);
}
if (relay.extHumidity != null) {
addChannel(thing, add, relay.extHumidity.sensor1 != null, CHGR_SENSOR, CHANNEL_ESENDOR_HUMIDITY);
}
return add;
}
public static Map<String, Channel> createRollerChannels(Thing thing, final ShellyControlRoller roller) {
Map<String, Channel> add = new LinkedHashMap<>();
addChannel(thing, add, roller.state != null, CHGR_ROLLER, CHANNEL_ROL_CONTROL_STATE);
return add;
}
public static Map<String, Channel> createMeterChannels(Thing thing, final ShellySettingsMeter meter, String group) {
Map<String, Channel> newChannels = new LinkedHashMap<>();
addChannel(thing, newChannels, meter.power != null, group, CHANNEL_METER_CURRENTWATTS);
addChannel(thing, newChannels, meter.total != null, group, CHANNEL_METER_TOTALKWH);
if (meter.counters != null) {
addChannel(thing, newChannels, meter.counters[0] != null, group, CHANNEL_METER_LASTMIN1);
}
addChannel(thing, newChannels, meter.timestamp != null, group, CHANNEL_LAST_UPDATE);
return newChannels;
}
public static Map<String, Channel> createEMeterChannels(final Thing thing, final ShellySettingsEMeter emeter,
String group) {
Map<String, Channel> newChannels = new LinkedHashMap<>();
addChannel(thing, newChannels, emeter.power != null, group, CHANNEL_METER_CURRENTWATTS);
addChannel(thing, newChannels, emeter.total != null, group, CHANNEL_METER_TOTALKWH);
addChannel(thing, newChannels, emeter.totalReturned != null, group, CHANNEL_EMETER_TOTALRET);
addChannel(thing, newChannels, emeter.reactive != null, group, CHANNEL_EMETER_REACTWATTS);
addChannel(thing, newChannels, emeter.voltage != null, group, CHANNEL_EMETER_VOLTAGE);
addChannel(thing, newChannels, emeter.current != null, group, CHANNEL_EMETER_CURRENT);
addChannel(thing, newChannels, emeter.pf != null, group, CHANNEL_EMETER_PFACTOR);
addChannel(thing, newChannels, true, group, CHANNEL_LAST_UPDATE);
return newChannels;
}
public static Map<String, Channel> createSensorChannels(final Thing thing, final ShellyDeviceProfile profile,
final ShellyStatusSensor sdata) {
Map<String, Channel> newChannels = new LinkedHashMap<>();
// Sensor data
addChannel(thing, newChannels, sdata.tmp != null, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_TEMP);
addChannel(thing, newChannels, sdata.hum != null, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_HUM);
addChannel(thing, newChannels, sdata.lux != null, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_LUX);
if (sdata.accel != null) {
addChannel(thing, newChannels, sdata.accel.vibration != null, CHANNEL_GROUP_SENSOR,
CHANNEL_SENSOR_VIBRATION);
addChannel(thing, newChannels, sdata.accel.tilt != null, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_TILT);
}
addChannel(thing, newChannels, sdata.flood != null, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_FLOOD);
addChannel(thing, newChannels, sdata.smoke != null, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_FLOOD);
addChannel(thing, newChannels, sdata.lux != null && sdata.lux.illumination != null, CHANNEL_GROUP_SENSOR,
CHANNEL_SENSOR_ILLUM);
addChannel(thing, newChannels, sdata.contact != null && sdata.contact.state != null, CHANNEL_GROUP_SENSOR,
CHANNEL_SENSOR_CONTACT);
addChannel(thing, newChannels, sdata.motion != null, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_MOTION);
addChannel(thing, newChannels, sdata.charger != null, CHGR_DEVST, CHANNEL_DEVST_CHARGER);
addChannel(thing, newChannels, sdata.sensorError != null, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_ERROR);
addChannel(thing, newChannels, sdata.actReasons != null, CHGR_DEVST, CHANNEL_DEVST_WAKEUP);
// Gas
if (sdata.gasSensor != null) {
addChannel(thing, newChannels, sdata.gasSensor.selfTestState != null, CHGR_DEVST, CHANNEL_DEVST_SELFTTEST);
addChannel(thing, newChannels, sdata.gasSensor.sensorState != null, CHANNEL_GROUP_SENSOR,
CHANNEL_SENSOR_SSTATE);
addChannel(thing, newChannels, sdata.concentration != null && sdata.concentration.ppm != null,
CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_PPM);
addChannel(thing, newChannels, sdata.valves != null, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_VALVE);
addChannel(thing, newChannels, sdata.gasSensor.sensorState != null, CHANNEL_GROUP_SENSOR,
CHANNEL_SENSOR_ALARM_STATE);
}
// Battery
if (sdata.bat != null) {
addChannel(thing, newChannels, sdata.bat.value != null, CHANNEL_GROUP_BATTERY, CHANNEL_SENSOR_BAT_LEVEL);
addChannel(thing, newChannels, sdata.bat.value != null, CHANNEL_GROUP_BATTERY, CHANNEL_SENSOR_BAT_LOW);
}
addChannel(thing, newChannels, true, profile.getControlGroup(0), CHANNEL_LAST_UPDATE);
return newChannels;
}
private static void addChannel(Thing thing, Map<String, Channel> newChannels, boolean supported, String group,
String channelName) throws IllegalArgumentException {
if (supported) {
final String channelId = group + "#" + channelName;
final ShellyChannel channelDef = getDefinition(channelId);
final ChannelUID channelUID = new ChannelUID(thing.getUID(), channelId);
final ChannelTypeUID channelTypeUID = channelDef.typeId.contains("system:")
? new ChannelTypeUID(channelDef.typeId)
: new ChannelTypeUID(BINDING_ID, channelDef.typeId);
Channel channel = ChannelBuilder.create(channelUID, channelDef.itemType).withType(channelTypeUID).build();
newChannels.put(channelId, channel);
}
}
public class ShellyChannel {
private final ShellyTranslationProvider messages;
public String group = "";
public String groupLabel = "";
public String groupDescription = "";
public String channel = "";
public String label = "";
public String description = "";
public String itemType = "";
public String typeId = "";
public String category = "";
public Set<String> tags = new HashSet<>();
public ShellyChannel(ShellyTranslationProvider messages, String group, String channel, String typeId,
String itemType, String... category) {
this.messages = messages;
this.group = group;
this.channel = channel;
this.itemType = itemType;
this.typeId = typeId;
groupLabel = getText(PREFIX_GROUP + group + SUFFIX_LABEL);
groupDescription = getText(PREFIX_GROUP + group + SUFFIX_DESCR);
label = getText(PREFIX_CHANNEL + channel + SUFFIX_LABEL);
description = getText(PREFIX_CHANNEL + channel + SUFFIX_DESCR);
}
public String getChanneId() {
return group + "#" + channel;
}
private String getText(String key) {
String text = messages.get(key);
return text != null ? text : "";
}
}
public static class ChannelMap {
private final Map<String, ShellyChannel> map = new LinkedHashMap<>();
private ChannelMap add(ShellyChannel def) {
map.put(def.getChanneId(), def);
return this;
}
public ShellyChannel get(String channelName) throws IllegalArgumentException {
ShellyChannel def = null;
if (channelName.contains("#")) {
def = map.get(channelName);
}
for (HashMap.Entry<String, ShellyChannel> entry : map.entrySet()) {
if (entry.getValue().channel.contains("#" + channelName)) {
def = entry.getValue();
break;
}
}
if (def == null) {
throw new IllegalArgumentException("Channel definition for " + channelName + " not found!");
}
return def;
}
}
}

View File

@@ -0,0 +1,186 @@
/**
* Copyright (c) 2010-2020 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.shelly.internal.handler;
import static org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.*;
import java.math.BigDecimal;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.library.types.HSBType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.PercentType;
/**
* The {@link ShellyColorUtils} provides some utility functions around RGBW handling.
*
* @author Markus Michels - Initial contribution
*/
@NonNullByDefault
public class ShellyColorUtils {
OnOffType power = OnOffType.OFF;
String mode = "";
int red = 0;
int green = 0;
int blue = 0;
int white = 0;
PercentType percentRed = new PercentType(0);
PercentType percentGreen = new PercentType(0);
PercentType percentBlue = new PercentType(0);
PercentType percentWhite = new PercentType(0);
int gain = 0;
int brightness = 0;
int temp = 0;
int minTemp = 0;
int maxTemp = 0;
PercentType percentGain = new PercentType(0);
PercentType percentBrightness = new PercentType(0);
PercentType percentTemp = new PercentType(0);
Integer effect = 0;
public ShellyColorUtils() {
}
public ShellyColorUtils(ShellyColorUtils col) {
minTemp = col.minTemp;
maxTemp = col.maxTemp;
setRed(col.red);
setGreen(col.green);
setBlue(col.blue);
setWhite(col.white);
setGain(col.gain);
setBrightness(col.brightness);
setTemp(col.temp);
}
void setMode(String mode) {
this.mode = mode;
}
void setMinMaxTemp(int min, int max) {
minTemp = min;
maxTemp = max;
}
boolean setRGBW(int red, int green, int blue, int white) {
setRed(red);
setGreen(green);
setBlue(blue);
setWhite(white);
return true;
}
boolean setRed(int value) {
boolean changed = red != value;
red = value;
percentRed = toPercent(red);
return changed;
}
boolean setGreen(int value) {
boolean changed = green != value;
green = value;
percentGreen = toPercent(green);
return changed;
}
boolean setBlue(int value) {
boolean changed = blue != value;
blue = value;
percentBlue = toPercent(blue);
return changed;
}
boolean setWhite(int value) {
boolean changed = white != value;
white = value;
percentWhite = toPercent(white);
return changed;
}
boolean setBrightness(int value) {
boolean changed = brightness != value;
brightness = value;
percentBrightness = toPercent(brightness, SHELLY_MIN_BRIGHTNESS, SHELLY_MAX_BRIGHTNESS);
return changed;
}
boolean setGain(int value) {
boolean changed = gain != value;
gain = value;
percentGain = toPercent(gain, SHELLY_MIN_GAIN, SHELLY_MAX_GAIN);
return changed;
}
boolean setTemp(int value) {
boolean changed = temp != value;
temp = value;
percentTemp = toPercent(temp, minTemp, maxTemp);
return changed;
}
boolean setEffect(int value) {
boolean changed = effect != value;
effect = value;
return changed;
}
public HSBType toHSB() {
return HSBType.fromRGB(red, green, blue);
}
public Integer[] fromRGBW(String rgbwString) {
Integer values[] = new Integer[4];
values[0] = values[1] = values[2] = values[3] = -1;
try {
String rgbw[] = rgbwString.split(",");
for (int i = 0; i < rgbw.length; i++) {
values[i] = Integer.parseInt(rgbw[i]);
}
} catch (NullPointerException e) { // might be a format problem
throw new IllegalArgumentException(
"Unable to convert fullColor value: " + rgbwString + ", " + e.getMessage());
}
if (values[0] != -1) {
setRed(values[0]);
}
if (values[1] != -1) {
setGreen(values[1]);
}
if (values[2] != -1) {
setBlue(values[2]);
}
if (values[3] != -1) {
setWhite(values[3]);
}
return values;
}
public static PercentType toPercent(Integer value) {
return toPercent(value, 0, SHELLY_MAX_COLOR);
}
public static PercentType toPercent(Integer _value, Integer min, Integer max) {
Double range = max.doubleValue() - min.doubleValue();
Double value = _value.doubleValue();
value = value < min ? min.doubleValue() : value;
value = value > max ? max.doubleValue() : value;
Double percent = 0.0;
if (range > 0) {
percent = new Double(Math.round((value - min) / range * 100));
}
return new PercentType(new BigDecimal(percent));
}
}

View File

@@ -0,0 +1,354 @@
/**
* Copyright (c) 2010-2020 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.shelly.internal.handler;
import static org.openhab.binding.shelly.internal.ShellyBindingConstants.*;
import static org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.*;
import static org.openhab.binding.shelly.internal.util.ShellyUtils.*;
import java.io.IOException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.shelly.internal.api.ShellyApiException;
import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsEMeter;
import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsMeter;
import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsStatus;
import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellyStatusSensor;
import org.openhab.binding.shelly.internal.api.ShellyDeviceProfile;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.OpenClosedType;
import org.openhab.core.library.unit.ImperialUnits;
import org.openhab.core.library.unit.SIUnits;
import org.openhab.core.library.unit.SmartHomeUnits;
import tec.uom.se.unit.Units;
/***
* The{@link ShellyComponents} implements updates for supplemental components
* Meter will be used by Relay + Light; Sensor is part of H&T, Flood, Door Window, Sense
*
* @author Markus Michels - Initial contribution
*/
@NonNullByDefault
public class ShellyComponents {
/**
* Update device status
*
* @param th Thing Handler instance
* @param profile ShellyDeviceProfile
*/
public static boolean updateDeviceStatus(ShellyBaseHandler thingHandler, ShellySettingsStatus status) {
if (!thingHandler.areChannelsCreated()) {
thingHandler.updateChannelDefinitions(ShellyChannelDefinitionsDTO
.createDeviceChannels(thingHandler.getThing(), thingHandler.getProfile(), status));
}
Integer rssi = getInteger(status.wifiSta.rssi);
thingHandler.updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_UPTIME,
toQuantityType(new Double(getLong(status.uptime)), DIGITS_NONE, SmartHomeUnits.SECOND));
thingHandler.updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_RSSI, mapSignalStrength(rssi));
if (status.tmp != null) {
thingHandler.updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_ITEMP,
toQuantityType(getDouble(status.tmp.tC), DIGITS_NONE, SIUnits.CELSIUS));
} else if (status.temperature != null) {
thingHandler.updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_ITEMP,
toQuantityType(getDouble(status.temperature), DIGITS_NONE, SIUnits.CELSIUS));
}
thingHandler.updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_UPDATE, getOnOff(status.hasUpdate));
return false; // device status never triggers update
}
/**
* Update Meter channel
*
* @param th Thing Handler instance
* @param profile ShellyDeviceProfile
* @param status Last ShellySettingsStatus
*/
public static boolean updateMeters(ShellyBaseHandler thingHandler, ShellySettingsStatus status) {
ShellyDeviceProfile profile = thingHandler.getProfile();
double accumulatedWatts = 0.0;
double accumulatedTotal = 0.0;
double accumulatedReturned = 0.0;
boolean updated = false;
// Devices without power meters get no updates
// We need to differ
// Roler+RGBW2 have multiple meters -> aggregate consumption to the functional device
// Meter and EMeter have a different set of channels
if ((profile.numMeters > 0) && ((status.meters != null) || (status.emeters != null))) {
if (!profile.isRoller && !profile.isRGBW2) {
thingHandler.logger.trace("{}: Updating {} {}meter(s)", thingHandler.thingName, profile.numMeters,
!profile.isEMeter ? "standard " : "e-");
// In Relay mode we map eacher meter to the matching channel group
int m = 0;
if (!profile.isEMeter) {
for (ShellySettingsMeter meter : status.meters) {
Integer meterIndex = m + 1;
if (getBool(meter.isValid) || profile.isLight) { // RGBW2-white doesn't report valid flag
// correctly in white mode
String groupName = "";
if (profile.numMeters > 1) {
groupName = CHANNEL_GROUP_METER + meterIndex.toString();
} else {
groupName = CHANNEL_GROUP_METER;
}
if (!thingHandler.areChannelsCreated()) {
// skip for Shelly Bulb: JSON has a meter, but values don't get updated
if (!profile.isBulb) {
thingHandler.updateChannelDefinitions(ShellyChannelDefinitionsDTO
.createMeterChannels(thingHandler.getThing(), meter, groupName));
}
}
updated |= thingHandler.updateChannel(groupName, CHANNEL_METER_CURRENTWATTS,
toQuantityType(getDouble(meter.power), DIGITS_WATT, SmartHomeUnits.WATT));
accumulatedWatts += getDouble(meter.power);
// convert Watt/Min to kw/h
if (meter.total != null) {
double kwh = getDouble(meter.total) / 60 / 1000;
updated |= thingHandler.updateChannel(groupName, CHANNEL_METER_TOTALKWH,
toQuantityType(kwh, DIGITS_KWH, SmartHomeUnits.KILOWATT_HOUR));
accumulatedTotal += kwh;
}
if (meter.counters != null) {
updated |= thingHandler.updateChannel(groupName, CHANNEL_METER_LASTMIN1,
toQuantityType(getDouble(meter.counters[0]), DIGITS_WATT, SmartHomeUnits.WATT));
}
thingHandler.updateChannel(groupName, CHANNEL_LAST_UPDATE,
getTimestamp(getString(profile.settings.timezone), getLong(meter.timestamp)));
}
m++;
}
} else {
for (ShellySettingsEMeter emeter : status.emeters) {
Integer meterIndex = m + 1;
if (getBool(emeter.isValid)) {
String groupName = profile.numMeters > 1 ? CHANNEL_GROUP_METER + meterIndex.toString()
: CHANNEL_GROUP_METER;
if (!thingHandler.areChannelsCreated()) {
thingHandler.updateChannelDefinitions(ShellyChannelDefinitionsDTO
.createEMeterChannels(thingHandler.getThing(), emeter, groupName));
}
// convert Watt/Hour tok w/h
updated |= thingHandler.updateChannel(groupName, CHANNEL_METER_CURRENTWATTS,
toQuantityType(getDouble(emeter.power), DIGITS_WATT, SmartHomeUnits.WATT));
updated |= thingHandler.updateChannel(groupName, CHANNEL_METER_TOTALKWH, toQuantityType(
getDouble(emeter.total) / 1000, DIGITS_KWH, SmartHomeUnits.KILOWATT_HOUR));
updated |= thingHandler.updateChannel(groupName, CHANNEL_EMETER_TOTALRET, toQuantityType(
getDouble(emeter.totalReturned) / 1000, DIGITS_KWH, SmartHomeUnits.KILOWATT_HOUR));
updated |= thingHandler.updateChannel(groupName, CHANNEL_EMETER_REACTWATTS,
toQuantityType(getDouble(emeter.reactive), DIGITS_WATT, SmartHomeUnits.WATT));
updated |= thingHandler.updateChannel(groupName, CHANNEL_EMETER_VOLTAGE,
toQuantityType(getDouble(emeter.voltage), DIGITS_VOLT, SmartHomeUnits.VOLT));
if (emeter.current != null) {
// Shelly
updated |= thingHandler.updateChannel(groupName, CHANNEL_EMETER_CURRENT,
toQuantityType(getDouble(emeter.current), DIGITS_VOLT, SmartHomeUnits.AMPERE));
updated |= thingHandler.updateChannel(groupName, CHANNEL_EMETER_PFACTOR,
getDecimal(emeter.pf));
}
accumulatedWatts += getDouble(emeter.power);
accumulatedTotal += getDouble(emeter.total) / 1000;
accumulatedReturned += getDouble(emeter.totalReturned) / 1000;
if (updated) {
thingHandler.updateChannel(groupName, CHANNEL_LAST_UPDATE, getTimestamp());
}
}
m++;
}
}
} else {
// In Roller Mode we accumulate all meters to a single set of meters
thingHandler.logger.trace("{}: Updating Meter (accumulated)", thingHandler.thingName);
double currentWatts = 0.0;
double totalWatts = 0.0;
double lastMin1 = 0.0;
long timestamp = 0l;
String groupName = CHANNEL_GROUP_METER;
for (ShellySettingsMeter meter : status.meters) {
if (meter.isValid) {
currentWatts += getDouble(meter.power);
totalWatts += getDouble(meter.total);
if (meter.counters != null) {
lastMin1 += getDouble(meter.counters[0]);
}
if (getLong(meter.timestamp) > timestamp) {
timestamp = getLong(meter.timestamp); // newest one
}
}
}
// Create channels for 1 Meter
if (!thingHandler.areChannelsCreated()) {
thingHandler.updateChannelDefinitions(ShellyChannelDefinitionsDTO
.createMeterChannels(thingHandler.getThing(), status.meters.get(0), groupName));
}
updated |= thingHandler.updateChannel(groupName, CHANNEL_METER_LASTMIN1,
toQuantityType(getDouble(lastMin1), DIGITS_WATT, SmartHomeUnits.WATT));
// convert totalWatts into kw/h
totalWatts = totalWatts / (60.0 * 10000.0);
updated |= thingHandler.updateChannel(groupName, CHANNEL_METER_CURRENTWATTS,
toQuantityType(getDouble(currentWatts), DIGITS_WATT, SmartHomeUnits.WATT));
updated |= thingHandler.updateChannel(groupName, CHANNEL_METER_TOTALKWH,
toQuantityType(getDouble(totalWatts), DIGITS_KWH, SmartHomeUnits.KILOWATT_HOUR));
if (updated) {
thingHandler.updateChannel(groupName, CHANNEL_LAST_UPDATE,
getTimestamp(getString(profile.settings.timezone), timestamp));
}
}
if (updated && !profile.isRoller && !profile.isRGBW2) {
thingHandler.updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_ACCUWATTS,
toQuantityType(accumulatedWatts, DIGITS_WATT, SmartHomeUnits.WATT));
thingHandler.updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_ACCUTOTAL,
toQuantityType(accumulatedTotal, DIGITS_KWH, SmartHomeUnits.KILOWATT_HOUR));
thingHandler.updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_ACCURETURNED,
toQuantityType(accumulatedReturned, DIGITS_KWH, SmartHomeUnits.KILOWATT_HOUR));
}
}
return updated;
}
/**
* Update Sensor channel
*
* @param th Thing Handler instance
* @param profile ShellyDeviceProfile
* @param status Last ShellySettingsStatus
*
* @throws IOException
*/
public static boolean updateSensors(ShellyBaseHandler thingHandler, ShellySettingsStatus status)
throws ShellyApiException {
ShellyDeviceProfile profile = thingHandler.getProfile();
boolean updated = false;
if (profile.isSensor || profile.hasBattery || profile.isSense) {
ShellyStatusSensor sdata = thingHandler.api.getSensorStatus();
if (!thingHandler.areChannelsCreated()) {
thingHandler.logger.trace("{}: Create missing sensor channel(s)", thingHandler.thingName);
thingHandler.updateChannelDefinitions(
ShellyChannelDefinitionsDTO.createSensorChannels(thingHandler.getThing(), profile, sdata));
}
updated |= thingHandler.updateWakeupReason(sdata.actReasons);
if ((sdata.contact != null) && sdata.contact.isValid) {
// Shelly DW: “sensor”:{“state”:“open”, “is_valid”:true},
updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_CONTACT,
getString(sdata.contact.state).equalsIgnoreCase(SHELLY_API_DWSTATE_OPEN) ? OpenClosedType.OPEN
: OpenClosedType.CLOSED);
boolean changed = thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_ERROR,
getStringType(sdata.sensorError));
if (changed) {
thingHandler.postEvent(sdata.sensorError, true);
}
updated |= changed;
}
if ((sdata.tmp != null) && getBool(sdata.tmp.isValid)) {
thingHandler.logger.trace("{}: Updating temperature", thingHandler.thingName);
Double temp = getString(sdata.tmp.units).toUpperCase().equals(SHELLY_TEMP_CELSIUS)
? getDouble(sdata.tmp.tC)
: getDouble(sdata.tmp.tF);
if (getString(sdata.tmp.units).toUpperCase().equals(SHELLY_TEMP_FAHRENHEIT)) {
// convert Fahrenheit to Celsius
temp = ImperialUnits.FAHRENHEIT.getConverterTo(Units.CELSIUS).convert(temp).doubleValue();
}
updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_TEMP,
toQuantityType(temp.doubleValue(), DIGITS_TEMP, SIUnits.CELSIUS));
}
if (sdata.hum != null) {
thingHandler.logger.trace("{}: Updating humidity", thingHandler.thingName);
updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_HUM,
toQuantityType(getDouble(sdata.hum.value), DIGITS_PERCENT, SmartHomeUnits.PERCENT));
}
if ((sdata.lux != null) && getBool(sdata.lux.isValid)) {
// “lux”:{“value”:30, “illumination”: “dark”, “is_valid”:true},
thingHandler.logger.trace("{}: Updating lux", thingHandler.thingName);
updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_LUX,
toQuantityType(getDouble(sdata.lux.value), DIGITS_LUX, SmartHomeUnits.LUX));
if (sdata.lux.illumination != null) {
updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_ILLUM,
getStringType(sdata.lux.illumination));
}
}
if (sdata.accel != null) {
updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_TILT, toQuantityType(
getDouble(sdata.accel.tilt.doubleValue()), DIGITS_NONE, SmartHomeUnits.DEGREE_ANGLE));
updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_VIBRATION,
getInteger(sdata.accel.vibration) == 1 ? OnOffType.ON : OnOffType.OFF);
}
if (sdata.flood != null) {
updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_FLOOD,
getOnOff(sdata.flood));
}
if (sdata.smoke != null) {
updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_SMOKE,
getOnOff(sdata.smoke));
}
if (sdata.gasSensor != null) {
updated |= thingHandler.updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_SELFTTEST,
getStringType(sdata.gasSensor.selfTestState));
updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_ALARM_STATE,
getStringType(sdata.gasSensor.alarmState));
updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_SSTATE,
getStringType(sdata.gasSensor.sensorState));
}
if ((sdata.concentration != null) && sdata.concentration.isValid) {
updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_PPM,
getDecimal(sdata.concentration.ppm));
}
if (sdata.bat != null) { // no update for Sense
thingHandler.logger.trace("{}: Updating battery", thingHandler.thingName);
updated |= thingHandler.updateChannel(CHANNEL_GROUP_BATTERY, CHANNEL_SENSOR_BAT_LEVEL,
toQuantityType(getDouble(sdata.bat.value), DIGITS_PERCENT, SmartHomeUnits.PERCENT));
boolean changed = thingHandler.updateChannel(CHANNEL_GROUP_BATTERY, CHANNEL_SENSOR_BAT_LOW,
getDouble(sdata.bat.value) < thingHandler.config.lowBattery ? OnOffType.ON : OnOffType.OFF);
updated |= changed;
if (changed && getDouble(sdata.bat.value) < thingHandler.config.lowBattery) {
thingHandler.postEvent(ALARM_TYPE_LOW_BATTERY, false);
}
}
if (sdata.motion != null) {
updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_MOTION,
getOnOff(sdata.motion));
}
if (sdata.charger != null) {
updated |= thingHandler.updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_CHARGER,
getOnOff(sdata.charger));
}
updated |= thingHandler.updateInputs(status);
if (updated) {
thingHandler.updateChannel(profile.getControlGroup(0), CHANNEL_LAST_UPDATE, getTimestamp());
}
}
return updated;
}
}

View File

@@ -0,0 +1,33 @@
/**
* Copyright (c) 2010-2020 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.shelly.internal.handler;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* {@link DeviceUpdateListener} can register on the {@link TradfriGatewayHandler} to be informed about details about
* devices.
*
* @author Markus Michels - Initial contribution
*/
@NonNullByDefault
public interface ShellyDeviceListener {
/**
* This method is called when new device information is received.
*/
public boolean onEvent(String ipAddress, String deviceName, String deviceIndex, String eventType,
Map<String, String> parameters);
}

View File

@@ -0,0 +1,512 @@
/**
* Copyright (c) 2010-2020 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.shelly.internal.handler;
import static org.openhab.binding.shelly.internal.ShellyBindingConstants.*;
import static org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.*;
import static org.openhab.binding.shelly.internal.util.ShellyUtils.*;
import java.util.Map;
import java.util.TreeMap;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jetty.client.HttpClient;
import org.openhab.binding.shelly.internal.api.ShellyApiException;
import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsStatus;
import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellyShortLightStatus;
import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellyStatusLight;
import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellyStatusLightChannel;
import org.openhab.binding.shelly.internal.api.ShellyDeviceProfile;
import org.openhab.binding.shelly.internal.coap.ShellyCoapServer;
import org.openhab.binding.shelly.internal.config.ShellyBindingConfiguration;
import org.openhab.binding.shelly.internal.util.ShellyTranslationProvider;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.HSBType;
import org.openhab.core.library.types.IncreaseDecreaseType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.PercentType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.library.unit.SmartHomeUnits;
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 ShellyLightHandler} handles light (Bulb, Duo and RGBW2) specific commands and status. All other commands
* will be routet of the ShellyBaseHandler.
*
* @author Markus Michels - Initial contribution
*/
@NonNullByDefault
public class ShellyLightHandler extends ShellyBaseHandler {
private final Logger logger = LoggerFactory.getLogger(ShellyLightHandler.class);
private final Map<Integer, ShellyColorUtils> channelColors;
/**
* Constructor
*
* @param thing The thing passed by the HandlerFactory
* @param bindingConfig configuration of the binding
* @param coapServer coap server instance
* @param localIP local IP of the openHAB host
* @param httpPort port of the openHAB HTTP API
*/
public ShellyLightHandler(final Thing thing, final ShellyTranslationProvider translationProvider,
final ShellyBindingConfiguration bindingConfig, final ShellyCoapServer coapServer, final String localIP,
int httpPort, final HttpClient httpClient) {
super(thing, translationProvider, bindingConfig, coapServer, localIP, httpPort, httpClient);
channelColors = new TreeMap<>();
}
@Override
public void initialize() {
logger.debug("Thing is using {}", this.getClass());
super.initialize();
}
@Override
public boolean handleDeviceCommand(ChannelUID channelUID, Command command) throws IllegalArgumentException {
String groupName = getString(channelUID.getGroupId());
if (groupName.isEmpty()) {
throw new IllegalArgumentException("Empty groupName");
}
int lightId = getLightIdFromGroup(groupName);
logger.trace("{}: Execute command {} on channel {}, lightId={}", thingName, command, channelUID.getAsString(),
lightId);
try {
ShellyColorUtils oldCol = getCurrentColors(lightId);
oldCol.mode = profile.mode;
ShellyColorUtils col = new ShellyColorUtils(oldCol);
boolean update = true;
switch (channelUID.getIdWithoutGroup()) {
default: // non-bulb commands will be handled by the generic handler
return false;
case CHANNEL_LIGHT_POWER:
logger.debug("{}: Switch light {}", thingName, command);
api.setLightParm(lightId, SHELLY_LIGHT_TURN,
command == OnOffType.ON ? SHELLY_API_ON : SHELLY_API_OFF);
col.power = (OnOffType) command;
requestUpdates(1, false);
update = false;
break;
case CHANNEL_LIGHT_COLOR_MODE:
logger.debug("{}: Select color mode {}", thingName, command);
col.setMode((OnOffType) command == OnOffType.ON ? SHELLY_MODE_COLOR : SHELLY_MODE_WHITE);
break;
case CHANNEL_COLOR_PICKER:
logger.debug("{}: Update colors from color picker", thingName);
update = handleColorPicker(profile, lightId, col, command);
break;
case CHANNEL_COLOR_FULL:
logger.debug("{}: Set colors to {}", thingName, command);
handleFullColor(col, command);
break;
case CHANNEL_COLOR_RED:
col.setRed(setColor(lightId, SHELLY_COLOR_RED, command, SHELLY_MAX_COLOR));
break;
case CHANNEL_COLOR_GREEN:
col.setGreen(setColor(lightId, SHELLY_COLOR_GREEN, command, SHELLY_MAX_COLOR));
break;
case CHANNEL_COLOR_BLUE:
col.setBlue(setColor(lightId, SHELLY_COLOR_BLUE, command, SHELLY_MAX_COLOR));
break;
case CHANNEL_COLOR_WHITE:
col.setWhite(setColor(lightId, SHELLY_COLOR_WHITE, command, SHELLY_MAX_COLOR));
break;
case CHANNEL_COLOR_GAIN:
col.setGain(setColor(lightId, SHELLY_COLOR_GAIN, command, SHELLY_MIN_GAIN, SHELLY_MAX_GAIN));
break;
case CHANNEL_BRIGHTNESS: // only in white mode
if (profile.inColor && !profile.isBulb) {
logger.debug("{}: Not in white mode, brightness not available", thingName);
break;
}
int value = -1;
if (command instanceof OnOffType) { // Switch
logger.debug("{}: Switch light {}", thingName, command);
ShellyShortLightStatus light = api.setRelayTurn(lightId,
command == OnOffType.ON ? SHELLY_API_ON : SHELLY_API_OFF);
col.power = getOnOff(light.ison);
col.setBrightness(light.brightness);
updateChannel(CHANNEL_COLOR_WHITE, CHANNEL_BRIGHTNESS + "$Switch", col.power);
updateChannel(CHANNEL_COLOR_WHITE, CHANNEL_BRIGHTNESS + "$Value",
toQuantityType(new Double(col.power == OnOffType.ON ? col.brightness : 0), DIGITS_NONE,
SmartHomeUnits.PERCENT));
update = false;
break;
}
if (command instanceof PercentType) {
Float percent = ((PercentType) command).floatValue();
value = percent.intValue(); // 0..100% = 0..100
logger.debug("{}: Set brightness to {}%/{}", thingName, percent, value);
} else if (command instanceof DecimalType) {
value = ((DecimalType) command).intValue();
logger.debug("{}: Set brightness to {} (Integer)", thingName, value);
}
if (value == 0) {
logger.debug("{}: Brightness=0 -> switch light OFF", thingName);
api.setRelayTurn(lightId, SHELLY_API_OFF);
update = false;
} else {
if (command instanceof IncreaseDecreaseType) {
ShellyShortLightStatus light = api.getLightStatus(lightId);
if (((IncreaseDecreaseType) command).equals(IncreaseDecreaseType.INCREASE)) {
value = Math.min(light.brightness + DIM_STEPSIZE, 100);
} else {
value = Math.max(light.brightness - DIM_STEPSIZE, 0);
}
logger.trace("{}: Change brightness from {} to {}", thingName, light.brightness, value);
}
validateRange("brightness", value, 0, 100);
logger.debug("{}: Changing brightness from {} to {}", thingName, oldCol.brightness, value);
col.setBrightness(value);
}
updateChannel(CHANNEL_GROUP_LIGHT_CONTROL, CHANNEL_LIGHT_POWER,
value > 0 ? OnOffType.ON : OnOffType.OFF);
break;
case CHANNEL_COLOR_TEMP:
Integer temp = -1;
if (command instanceof PercentType) {
logger.debug("{}: Set color temp to {}%", thingName, ((PercentType) command).floatValue());
Float percent = ((PercentType) command).floatValue() / 100;
temp = new DecimalType(col.minTemp + ((col.maxTemp - col.minTemp)) * percent).intValue();
logger.debug("{}: Converted color-temp {}% to {}K (from Percent to Integer)", thingName,
percent, temp);
} else if (command instanceof DecimalType) {
temp = ((DecimalType) command).intValue();
logger.debug("{}: Set color temp to {}K (Integer)", thingName, temp);
}
validateRange(CHANNEL_COLOR_TEMP, temp, col.minTemp, col.maxTemp);
col.setTemp(temp);
col.brightness = -1;
break;
case CHANNEL_COLOR_EFFECT:
Integer effect = ((DecimalType) command).intValue();
logger.debug("{}: Set color effect to {}", thingName, effect);
validateRange("effect", effect, SHELLY_MIN_EFFECT, SHELLY_MAX_EFFECT);
col.setEffect(effect.intValue());
}
if (update) {
// check for switching color mode
if (profile.isBulb && !col.mode.isEmpty() && !col.mode.equals(oldCol.mode)) {
logger.debug("{}: Color mode changed from {} to {}, set new mode", thingName, oldCol.mode,
col.mode);
api.setLightMode(col.mode);
}
// send changed colors to the device
sendColors(profile, lightId, oldCol, col, config.brightnessAutoOn);
}
return true;
} catch (ShellyApiException e) {
logger.debug("{}: Unable to handle command: {}", thingName, e.toString());
return false;
} catch (IllegalArgumentException e) {
logger.debug("{}: Unable to handle command", thingName, e);
return false;
}
}
private boolean handleColorPicker(ShellyDeviceProfile profile, Integer lightId, ShellyColorUtils col,
Command command) throws ShellyApiException {
boolean updated = false;
if (command instanceof HSBType) {
HSBType hsb = (HSBType) command;
logger.debug("HSB-Info={}, Hue={}, getRGB={}, toRGB={}/{}/{}", hsb, hsb.getHue(),
String.format("0x%08X", hsb.getRGB()), hsb.toRGB()[0], hsb.toRGB()[1], hsb.toRGB()[2]);
if (hsb.toString().contains("360,")) {
logger.trace("{}: need to fix the Hue value (360->0)", thingName);
HSBType fixHue = new HSBType(new DecimalType(0), hsb.getSaturation(), hsb.getBrightness());
hsb = fixHue;
}
col.setRed(getColorFromHSB(hsb.getRed()));
col.setBlue(getColorFromHSB(hsb.getBlue()));
col.setGreen(getColorFromHSB(hsb.getGreen()));
col.setBrightness(getColorFromHSB(hsb.getBrightness(), BRIGHTNESS_FACTOR));
// white, gain and temp are not part of the HSB color scheme
updated = true;
} else if (command instanceof PercentType) {
if (!profile.inColor || profile.isBulb) {
col.brightness = SHELLY_MAX_BRIGHTNESS * ((PercentType) command).intValue();
updated = true;
}
} else if (command instanceof OnOffType) {
logger.debug("{}: Switch light {}", thingName, command);
api.setLightParm(lightId, SHELLY_LIGHT_TURN,
(OnOffType) command == OnOffType.ON ? SHELLY_API_ON : SHELLY_API_OFF);
col.power = (OnOffType) command;
} else if (command instanceof IncreaseDecreaseType) {
if (!profile.inColor || profile.isBulb) {
logger.debug("{}: {} brightness by {}", thingName, command, SHELLY_DIM_STEPSIZE);
PercentType percent = (PercentType) super.getChannelValue(CHANNEL_GROUP_COLOR_CONTROL,
CHANNEL_BRIGHTNESS);
if (percent != null) {
int currentBrightness = percent.intValue() * SHELLY_MAX_BRIGHTNESS;
int newBrightness = currentBrightness;
if (command == IncreaseDecreaseType.DECREASE) {
newBrightness = Math.max(currentBrightness - SHELLY_DIM_STEPSIZE, 0);
} else {
newBrightness = Math.min(currentBrightness + SHELLY_DIM_STEPSIZE, SHELLY_MAX_BRIGHTNESS);
}
col.brightness = newBrightness;
updated = currentBrightness != newBrightness;
}
}
}
return updated;
}
private boolean handleFullColor(ShellyColorUtils col, Command command) throws IllegalArgumentException {
String color = command.toString().toLowerCase();
if (color.contains(",")) {
col.fromRGBW(color);
} else if (color.equals(SHELLY_COLOR_RED)) {
col.setRGBW(SHELLY_MAX_COLOR, 0, 0, 0);
} else if (color.equals(SHELLY_COLOR_GREEN)) {
col.setRGBW(0, SHELLY_MAX_COLOR, 0, 0);
} else if (color.equals(SHELLY_COLOR_BLUE)) {
col.setRGBW(0, 0, SHELLY_MAX_COLOR, 0);
} else if (color.equals(SHELLY_COLOR_YELLOW)) {
col.setRGBW(SHELLY_MAX_COLOR, SHELLY_MAX_COLOR, 0, 0);
} else if (color.equals(SHELLY_COLOR_WHITE)) {
col.setRGBW(0, 0, 0, SHELLY_MAX_COLOR);
col.setMode(SHELLY_MODE_WHITE);
} else {
throw new IllegalArgumentException("Invalid full color selection: " + color);
}
col.setMode(color.equals(SHELLY_MODE_WHITE) ? SHELLY_MODE_WHITE : SHELLY_MODE_COLOR);
return true;
}
private ShellyColorUtils getCurrentColors(int lightId) {
ShellyColorUtils col;
if (!channelColors.containsKey(lightId)) {
col = new ShellyColorUtils(); // create a new entry
col.setMinMaxTemp(profile.minTemp, profile.maxTemp);
channelColors.put(lightId, col);
logger.trace("{}: Colors entry created for lightId {}", thingName, lightId);
} else {
col = channelColors.get(lightId);
logger.trace(
"{}: Colors loaded for lightId {}: power={}, RGBW={}/{}/{}/{}, gain={}, brightness={}, color temp={} (min={}, max={}",
thingName, lightId, col.power, col.red, col.green, col.blue, col.white, col.gain, col.brightness,
col.temp, col.minTemp, col.maxTemp);
}
return col;
}
@Override
public boolean updateDeviceStatus(ShellySettingsStatus genericStatus) throws ShellyApiException {
if (!profile.isInitialized()) {
logger.debug("{}: Device not yet initialized!", thingName);
return false;
}
if (!profile.isLight) {
logger.debug("{}: ERROR: Device is not a light. but class ShellyHandlerLight is called!", thingName);
}
ShellyStatusLight status = api.getLightStatus();
logger.trace("{}: Updating light status in {} mode, {} channel(s)", thingName, profile.mode,
status.lights.size());
// In white mode we have multiple channels
int lightId = 0;
boolean updated = false;
for (ShellyStatusLightChannel light : status.lights) {
Integer channelId = lightId + 1;
String controlGroup = buildControlGroupName(profile, channelId);
// The bulb has a combined channel set for color or white mode
// The RGBW2 uses 2 different thing types: color=1 channel, white=4 channel
if (profile.isBulb) {
updateChannel(CHANNEL_GROUP_LIGHT_CONTROL, CHANNEL_LIGHT_COLOR_MODE, getOnOff(profile.inColor));
}
ShellyColorUtils col = getCurrentColors(lightId);
col.power = getOnOff(light.ison);
// Channel control/timer
// ShellyStatusLightChannel light = status.lights.get(i);
updated |= updateChannel(controlGroup, CHANNEL_LIGHT_POWER, col.power);
updated |= updateChannel(controlGroup, CHANNEL_TIMER_AUTOON, getDecimal(light.autoOn));
updated |= updateChannel(controlGroup, CHANNEL_TIMER_AUTOOFF, getDecimal(light.autoOff));
updated |= updateInputs(controlGroup, genericStatus, lightId);
if (getBool(light.overpower)) {
postEvent(ALARM_TYPE_OVERPOWER, false);
}
if (profile.inColor) {
logger.trace("{}: update color settings", thingName);
col.setRGBW(getInteger(light.red), getInteger(light.green), getInteger(light.blue),
getInteger(light.white));
col.setGain(getInteger(light.gain));
col.setEffect(getInteger(light.effect));
String colorGroup = CHANNEL_GROUP_COLOR_CONTROL;
logger.trace("{}: Update channels for group {}: RGBW={}/{}/{}, in %:{}%/{}%/{}%, white={}%, gain={}%",
thingName, colorGroup, col.red, col.green, col.blue, col.percentRed, col.percentGreen,
col.percentBlue, col.percentWhite, col.percentGain);
updated |= updateChannel(colorGroup, CHANNEL_COLOR_RED, col.percentRed);
updated |= updateChannel(colorGroup, CHANNEL_COLOR_GREEN, col.percentGreen);
updated |= updateChannel(colorGroup, CHANNEL_COLOR_BLUE, col.percentBlue);
updated |= updateChannel(colorGroup, CHANNEL_COLOR_WHITE, col.percentWhite);
updated |= updateChannel(colorGroup, CHANNEL_COLOR_GAIN, col.percentGain);
updated |= updateChannel(colorGroup, CHANNEL_COLOR_EFFECT, new DecimalType(col.effect));
setFullColor(colorGroup, col);
logger.trace("{}: update {}.color picker", thingName, colorGroup);
updated |= updateChannel(colorGroup, CHANNEL_COLOR_PICKER, col.toHSB());
}
if (!profile.inColor || profile.isBulb) {
String whiteGroup = buildWhiteGroupName(profile, channelId);
col.setBrightness(getInteger(light.brightness));
updated |= updateChannel(whiteGroup, CHANNEL_BRIGHTNESS + "$Switch", col.power);
updated |= updateChannel(whiteGroup, CHANNEL_BRIGHTNESS + "$Value",
toQuantityType(col.power == OnOffType.ON ? col.percentBrightness.doubleValue() : new Double(0),
DIGITS_NONE, SmartHomeUnits.PERCENT));
if ((profile.isBulb || profile.isDuo) && (light.temp != null)) {
col.setTemp(getInteger(light.temp));
updated |= updateChannel(whiteGroup, CHANNEL_COLOR_TEMP, col.percentTemp);
logger.trace("{}: update {}.color picker", thingName, whiteGroup);
updated |= updateChannel(whiteGroup, CHANNEL_COLOR_PICKER, col.toHSB());
}
}
// continue with next light
lightId++;
}
return updated;
}
private Integer setColor(Integer lightId, String colorName, Command command, Integer minValue, Integer maxValue)
throws ShellyApiException, IllegalArgumentException {
Integer value = -1;
logger.debug("{}: Set {} to {} ({})", thingName, colorName, command, command.getClass());
if (command instanceof PercentType) {
PercentType percent = (PercentType) command;
Double v = new Double(maxValue) * percent.doubleValue() / 100.0;
value = v.intValue();
logger.debug("{}: Value for {} is in percent: {}%={}", thingName, colorName, percent, value);
} else if (command instanceof DecimalType) {
value = ((DecimalType) command).intValue();
logger.debug("Value for {} is a number: {}", colorName, value);
} else if (command instanceof OnOffType) {
value = ((OnOffType) command).equals(OnOffType.ON) ? SHELLY_MAX_COLOR : SHELLY_MIN_COLOR;
logger.debug("{}: Value for {} of type OnOff was converted to {}", thingName, colorName, value);
} else {
throw new IllegalArgumentException(
"Invalid value type for " + colorName + ": " + value + " / type " + value.getClass());
}
validateRange(colorName, value, minValue, maxValue);
return value.intValue();
}
private Integer setColor(Integer lightId, String colorName, Command command, Integer maxValue)
throws ShellyApiException, IllegalArgumentException {
return setColor(lightId, colorName, command, 0, maxValue);
}
private void setFullColor(String colorGroup, ShellyColorUtils col) {
if ((col.red == SHELLY_MAX_COLOR) && (col.green == SHELLY_MAX_COLOR) && (col.blue == 0)) {
updateChannel(colorGroup, CHANNEL_COLOR_FULL, new StringType(SHELLY_COLOR_YELLOW));
} else if ((col.red == SHELLY_MAX_COLOR) && (col.green == 0) && (col.blue == 0)) {
updateChannel(colorGroup, CHANNEL_COLOR_FULL, new StringType(SHELLY_COLOR_RED));
} else if ((col.red == 0) && (col.green == SHELLY_MAX_COLOR) && (col.blue == 0)) {
updateChannel(colorGroup, CHANNEL_COLOR_FULL, new StringType(SHELLY_COLOR_GREEN));
} else if ((col.red == 0) && (col.green == 0) && (col.blue == SHELLY_MAX_COLOR)) {
updateChannel(colorGroup, CHANNEL_COLOR_FULL, new StringType(SHELLY_COLOR_BLUE));
} else if ((col.red == 0) && (col.green == 0) && (col.blue == 0) && (col.white == SHELLY_MAX_COLOR)) {
updateChannel(colorGroup, CHANNEL_COLOR_FULL, new StringType(SHELLY_COLOR_WHITE));
}
}
private void sendColors(ShellyDeviceProfile profile, Integer lightId, ShellyColorUtils oldCol,
ShellyColorUtils newCol, boolean autoOn) throws ShellyApiException {
// boolean updated = false;
Integer channelId = lightId + 1;
Map<String, String> parms = new TreeMap<>();
logger.trace(
"{}: New color settings for channel {}: RGB {}/{}/{}, white={}, gain={}, brightness={}, color-temp={}",
thingName, channelId, newCol.red, newCol.green, newCol.blue, newCol.white, newCol.gain,
newCol.brightness, newCol.temp);
if (autoOn && (newCol.brightness >= 0)) {
parms.put(SHELLY_LIGHT_TURN, profile.inColor || newCol.brightness > 0 ? SHELLY_API_ON : SHELLY_API_OFF);
}
if (profile.inColor) {
if (oldCol.red != newCol.red || oldCol.green != newCol.green || oldCol.blue != newCol.blue
|| oldCol.white != newCol.white) {
logger.debug("{}: Setting RGBW to {}/{}/{}/{}", thingName, newCol.red, newCol.green, newCol.blue,
newCol.white);
parms.put(SHELLY_COLOR_RED, String.valueOf(newCol.red));
parms.put(SHELLY_COLOR_GREEN, String.valueOf(newCol.green));
parms.put(SHELLY_COLOR_BLUE, String.valueOf(newCol.blue));
parms.put(SHELLY_COLOR_WHITE, String.valueOf(newCol.white));
}
}
if ((!profile.inColor) && (oldCol.temp != newCol.temp)) {
logger.debug("{}: Setting color temp to {}", thingName, newCol.temp);
parms.put(SHELLY_COLOR_TEMP, String.valueOf(newCol.temp));
}
if (oldCol.gain != newCol.gain) {
logger.debug("{}: Setting gain to {}", thingName, newCol.gain);
parms.put(SHELLY_COLOR_GAIN, String.valueOf(newCol.gain));
}
if ((newCol.brightness >= 0) && (!profile.inColor || profile.isBulb)
&& (oldCol.brightness != newCol.brightness)) {
logger.debug("{}: Setting brightness to {}", thingName, newCol.brightness);
parms.put(SHELLY_COLOR_BRIGHTNESS, String.valueOf(newCol.brightness));
}
if (!oldCol.effect.equals(newCol.effect)) {
logger.debug("{}: Setting effect to {}", thingName, newCol.effect);
parms.put(SHELLY_COLOR_EFFECT, newCol.effect.toString());
}
if (parms.size() > 0) {
logger.debug("{}: Send light settings: {}", thingName, parms);
api.setLightParms(lightId, parms);
updateCurrentColors(lightId, newCol);
}
}
private void updateCurrentColors(int lightId, ShellyColorUtils col) {
channelColors.replace(lightId, col);
logger.debug("{}: Colors updated for lightId {}: RGBW={}/{}/{}/{}, Sat/Gain={}, Bright={}, Temp={} ", thingName,
lightId, col.red, col.green, col.blue, col.white, col.gain, col.brightness, col.temp);
}
private Integer getColorFromHSB(PercentType colorPercent) {
return getColorFromHSB(colorPercent, new Double(SATURATION_FACTOR));
}
private Integer getColorFromHSB(PercentType colorPercent, Double factor) {
Double value = new Double(Math.round(colorPercent.doubleValue() * factor));
logger.trace("{}: convert {}% into {}/{} (factor={})", thingName, colorPercent, value, value.intValue(),
factor);
return value.intValue();
}
}

View File

@@ -0,0 +1,52 @@
/**
* Copyright (c) 2010-2020 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.shelly.internal.handler;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jetty.client.HttpClient;
import org.openhab.binding.shelly.internal.coap.ShellyCoapServer;
import org.openhab.binding.shelly.internal.config.ShellyBindingConfiguration;
import org.openhab.binding.shelly.internal.util.ShellyTranslationProvider;
import org.openhab.core.thing.Thing;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link ShellyProtectedHandler} implements a dummy handler for password protected devices.
*
* @author Markus Michels - Initial contribution
*/
@NonNullByDefault
public class ShellyProtectedHandler extends ShellyBaseHandler {
private final Logger logger = LoggerFactory.getLogger(ShellyProtectedHandler.class);
/**
* Constructor
*
* @param thing The thing passed by the HandlerFactory
* @param bindingConfig configuration of the binding
* @param coapServer coap server instance
* @param localIP local IP of the openHAB host
* @param httpPort port of the openHAB HTTP API
*/
public ShellyProtectedHandler(final Thing thing, final ShellyTranslationProvider translationProvider,
final ShellyBindingConfiguration bindingConfig, final ShellyCoapServer coapServer, final String localIP,
int httpPort, final HttpClient httpClient) {
super(thing, translationProvider, bindingConfig, coapServer, localIP, httpPort, httpClient);
}
@Override
public void initialize() {
super.initialize();
}
}

View File

@@ -0,0 +1,463 @@
/**
* Copyright (c) 2010-2020 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.shelly.internal.handler;
import static org.openhab.binding.shelly.internal.ShellyBindingConstants.*;
import static org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.*;
import static org.openhab.binding.shelly.internal.util.ShellyUtils.*;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jetty.client.HttpClient;
import org.openhab.binding.shelly.internal.api.ShellyApiException;
import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO;
import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellyControlRoller;
import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsDimmer;
import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsRelay;
import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsRoller;
import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsStatus;
import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellyShortLightStatus;
import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellyShortStatusRelay;
import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellyStatusRelay;
import org.openhab.binding.shelly.internal.coap.ShellyCoapServer;
import org.openhab.binding.shelly.internal.config.ShellyBindingConfiguration;
import org.openhab.binding.shelly.internal.util.ShellyTranslationProvider;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.IncreaseDecreaseType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.PercentType;
import org.openhab.core.library.types.StopMoveType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.library.types.UpDownType;
import org.openhab.core.library.unit.SIUnits;
import org.openhab.core.library.unit.SmartHomeUnits;
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;
import com.google.gson.Gson;
/***
* The{@link ShellyRelayHandler} handles light (bulb+rgbw2) specific commands and status. All other commands will be
* handled by the generic thing handler.
*
* @author Markus Michels - Initial contribution
*/
@NonNullByDefault
public class ShellyRelayHandler extends ShellyBaseHandler {
private final Logger logger = LoggerFactory.getLogger(ShellyRelayHandler.class);
/**
* Constructor
*
* @param thing The thing passed by the HandlerFactory
* @param bindingConfig configuration of the binding
* @param coapServer coap server instance
* @param localIP local IP of the openHAB host
* @param httpPort port of the openHAB HTTP API
*/
public ShellyRelayHandler(final Thing thing, final ShellyTranslationProvider translationProvider,
final ShellyBindingConfiguration bindingConfig, final ShellyCoapServer coapServer, final String localIP,
int httpPort, final HttpClient httpClient) {
super(thing, translationProvider, bindingConfig, coapServer, localIP, httpPort, httpClient);
}
@Override
public void initialize() {
super.initialize();
}
@Override
public boolean handleDeviceCommand(ChannelUID channelUID, Command command) throws ShellyApiException {
// Process command
String groupName = getString(channelUID.getGroupId());
Integer rIndex = 0;
if (groupName.startsWith(CHANNEL_GROUP_RELAY_CONTROL)
&& groupName.length() > CHANNEL_GROUP_RELAY_CONTROL.length()) {
rIndex = Integer.parseInt(substringAfter(channelUID.getGroupId(), CHANNEL_GROUP_RELAY_CONTROL)) - 1;
} else if (groupName.startsWith(CHANNEL_GROUP_ROL_CONTROL)
&& groupName.length() > CHANNEL_GROUP_ROL_CONTROL.length()) {
rIndex = Integer.parseInt(substringAfter(channelUID.getGroupId(), CHANNEL_GROUP_ROL_CONTROL)) - 1;
}
switch (channelUID.getIdWithoutGroup()) {
default:
return false;
case CHANNEL_OUTPUT:
if (!profile.isRoller) {
// extract relay number of group name (relay0->0, relay1->1...)
logger.debug("{}: Set relay output to {}", thingName, command);
api.setRelayTurn(rIndex, command == OnOffType.ON ? SHELLY_API_ON : SHELLY_API_OFF);
} else {
logger.debug("{}: Device is in roller mode, channel command {} ignored", thingName, channelUID);
}
break;
case CHANNEL_BRIGHTNESS: // e.g.Dimmer, Duo
handleBrightness(command, rIndex);
break;
case CHANNEL_ROL_CONTROL_POS:
case CHANNEL_ROL_CONTROL_CONTROL:
logger.debug("{}: Roller command/position {}", thingName, command);
handleRoller(command, groupName, rIndex,
channelUID.getIdWithoutGroup().equals(CHANNEL_ROL_CONTROL_CONTROL));
// request updates the next 45sec to update roller position after it stopped
requestUpdates(autoCoIoT ? 1 : 45 / UPDATE_STATUS_INTERVAL_SECONDS, false);
break;
case CHANNEL_TIMER_AUTOON:
logger.debug("{}: Set Auto-ON timer to {}", thingName, command);
api.setTimer(rIndex, SHELLY_TIMER_AUTOON, getNumber(command));
break;
case CHANNEL_TIMER_AUTOOFF:
logger.debug("{}: Set Auto-OFF timer to {}", thingName, command);
api.setTimer(rIndex, SHELLY_TIMER_AUTOOFF, getNumber(command));
break;
}
return true;
}
/**
* PaperUI Control has a combined Slider for Brightness combined with On/Off
* Brightness channel has 2 functions: Switch On/Off (OnOnType) and setting brightness (PercentType)
* There is some more logic in the control. When brightness is set to 0 the control sends also an OFF command
* When current brightness is 0 and slider will be moved the new brightness will be set, but also a ON command is
* send.
*
* @param command
* @param index
* @throws ShellyApiException
*/
private void handleBrightness(Command command, Integer index) throws ShellyApiException {
Integer value = -1;
if (command instanceof PercentType) { // Dimmer
value = ((PercentType) command).intValue();
} else if (command instanceof DecimalType) { // Number
value = ((DecimalType) command).intValue();
} else if (command instanceof OnOffType) { // Switch
logger.debug("{}: Switch output {}", thingName, command);
updateBrightnessChannel(index, (OnOffType) command, value);
return;
} else if (command instanceof IncreaseDecreaseType) {
ShellyShortLightStatus light = api.getLightStatus(index);
if (((IncreaseDecreaseType) command).equals(IncreaseDecreaseType.INCREASE)) {
value = Math.min(light.brightness + DIM_STEPSIZE, 100);
} else {
value = Math.max(light.brightness - DIM_STEPSIZE, 0);
}
logger.debug("{}: Increase/Decrease brightness from {} to {}", thingName, light.brightness, value);
}
validateRange("brightness", value, 0, 100);
// Switch light off on brightness = 0
if (value == 0) {
logger.debug("{}: Brightness=0 -> switch output OFF", thingName);
updateBrightnessChannel(index, OnOffType.OFF, 0);
} else {
logger.debug("{}: Setting dimmer brightness to {}", thingName, value);
updateBrightnessChannel(index, OnOffType.ON, value);
}
}
private void updateBrightnessChannel(int lightId, OnOffType power, int brightness) throws ShellyApiException {
if (brightness > 0) {
api.setBrightness(lightId, brightness, config.brightnessAutoOn);
} else {
api.setRelayTurn(lightId, power == OnOffType.ON ? SHELLY_API_ON : SHELLY_API_OFF);
}
updateChannel(CHANNEL_COLOR_WHITE, CHANNEL_BRIGHTNESS + "$Switch", power);
updateChannel(CHANNEL_COLOR_WHITE, CHANNEL_BRIGHTNESS + "$Value", toQuantityType(
new Double(power == OnOffType.ON ? brightness : 0), DIGITS_NONE, SmartHomeUnits.PERCENT));
}
@Override
public boolean updateDeviceStatus(ShellySettingsStatus status) throws ShellyApiException {
// map status to channels
boolean updated = false;
updated |= updateRelays(status);
updated |= updateDimmers(status);
updated |= updateLed(status);
return updated;
}
/**
* Handle Roller Commands
*
* @param command from handleCommand()
* @param groupName relay, roller...
* @param index relay number
* @param isControl true: is the Rollershutter channel, false: rollerpos channel
* @throws ShellyApiException
*/
private void handleRoller(Command command, String groupName, Integer index, boolean isControl)
throws ShellyApiException {
Integer position = -1;
if ((command instanceof UpDownType) || (command instanceof OnOffType)) {
ShellyControlRoller rstatus = api.getRollerStatus(index);
if (!getString(rstatus.state).isEmpty() && !getString(rstatus.state).equals(SHELLY_ALWD_ROLLER_TURN_STOP)) {
boolean up = command instanceof UpDownType && (UpDownType) command == UpDownType.UP;
boolean down = command instanceof UpDownType && (UpDownType) command == UpDownType.DOWN;
if ((up && getString(rstatus.state).equals(SHELLY_ALWD_ROLLER_TURN_OPEN))
|| (down && getString(rstatus.state).equals(SHELLY_ALWD_ROLLER_TURN_CLOSE))) {
logger.debug("{}: Roller is already moving ({}), ignore command {}", thingName,
getString(rstatus.state), command);
requestUpdates(1, false);
return;
}
}
if (((command instanceof UpDownType) && UpDownType.UP.equals(command))
|| ((command instanceof OnOffType) && OnOffType.ON.equals(command))) {
logger.debug("{}: Open roller", thingName);
api.setRollerTurn(index, SHELLY_ALWD_ROLLER_TURN_OPEN);
position = SHELLY_MAX_ROLLER_POS;
}
if (((command instanceof UpDownType) && UpDownType.DOWN.equals(command))
|| ((command instanceof OnOffType) && OnOffType.OFF.equals(command))) {
logger.debug("{}: Closing roller", thingName);
api.setRollerTurn(index, SHELLY_ALWD_ROLLER_TURN_CLOSE);
position = SHELLY_MIN_ROLLER_POS;
}
} else if ((command instanceof StopMoveType) && StopMoveType.STOP.equals(command)) {
logger.debug("{}: Stop roller", thingName);
api.setRollerTurn(index, SHELLY_ALWD_ROLLER_TURN_STOP);
} else {
logger.debug("{}: Set roller to position {}", thingName, command);
if (command instanceof PercentType) {
PercentType p = (PercentType) command;
position = p.intValue();
} else if (command instanceof DecimalType) {
DecimalType d = (DecimalType) command;
position = d.intValue();
} else {
throw new IllegalArgumentException(
"Invalid value type for roller control/posiution" + command.getClass().toString());
}
// take position from RollerShutter control and map to Shelly positon (OH:
// 0=closed, 100=open; Shelly 0=open, 100=closed)
// take position 1:1 from position channel
position = isControl ? SHELLY_MAX_ROLLER_POS - position : position;
validateRange("roller position", position, SHELLY_MIN_ROLLER_POS, SHELLY_MAX_ROLLER_POS);
logger.debug("{}: Changing roller position to {}", thingName, position);
api.setRollerPos(index, position);
}
if (position != -1) {
// make sure both are in sync
if (isControl) {
int pos = SHELLY_MAX_ROLLER_POS - Math.max(0, Math.min(position, SHELLY_MAX_ROLLER_POS));
updateChannel(groupName, CHANNEL_ROL_CONTROL_CONTROL, new PercentType(pos));
} else {
updateChannel(groupName, CHANNEL_ROL_CONTROL_POS, new PercentType(position));
}
}
}
/**
* Auto-create relay channels depending on relay type/mode
*/
private void createRelayChannels(ShellyStatusRelay relay, int idx) {
if (!areChannelsCreated()) {
updateChannelDefinitions(ShellyChannelDefinitionsDTO.createRelayChannels(getThing(), profile, relay, idx));
}
}
private void createRollerChannels(ShellyControlRoller roller) {
if (!areChannelsCreated()) {
updateChannelDefinitions(ShellyChannelDefinitionsDTO.createRollerChannels(getThing(), roller));
}
}
/**
* Update Relay/Roller channels
*
* @param th Thing Handler instance
* @param profile ShellyDeviceProfile
* @param status Last ShellySettingsStatus
*
* @throws ShellyApiException
*/
public boolean updateRelays(ShellySettingsStatus status) throws ShellyApiException {
boolean updated = false;
// Check for Relay in Standard Mode
if (profile.hasRelays && !profile.isRoller && !profile.isDimmer) {
logger.trace("{}: Updating {} relay(s)", thingName, profile.numRelays);
int i = 0;
ShellyStatusRelay rstatus = api.getRelayStatus(i);
for (ShellyShortStatusRelay relay : rstatus.relays) {
createRelayChannels(rstatus, i);
if ((relay.isValid == null) || relay.isValid) {
String groupName = profile.getControlGroup(i);
ShellySettingsRelay rs = profile.settings.relays.get(i);
updated |= updateChannel(groupName, CHANNEL_OUTPUT_NAME, getStringType(rs.name));
if (getBool(relay.overpower)) {
postEvent(ALARM_TYPE_OVERPOWER, false);
}
updated |= updateChannel(groupName, CHANNEL_OUTPUT, getOnOff(relay.ison));
updated |= updateChannel(groupName, CHANNEL_TIMER_ACTIVE, getOnOff(relay.hasTimer));
if (rstatus.extTemperature != null) {
// Shelly 1/1PM support up to 3 external sensors
// for whatever reason those are not represented as an array, but 3 elements
if (rstatus.extTemperature.sensor1 != null) {
updated |= updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_ESENDOR_TEMP1, toQuantityType(
getDouble(rstatus.extTemperature.sensor1.tC), DIGITS_TEMP, SIUnits.CELSIUS));
}
if (rstatus.extTemperature.sensor2 != null) {
updated |= updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_ESENDOR_TEMP2, toQuantityType(
getDouble(rstatus.extTemperature.sensor2.tC), DIGITS_TEMP, SIUnits.CELSIUS));
}
if (rstatus.extTemperature.sensor3 != null) {
updated |= updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_ESENDOR_TEMP3, toQuantityType(
getDouble(rstatus.extTemperature.sensor3.tC), DIGITS_TEMP, SIUnits.CELSIUS));
}
}
if ((rstatus.extHumidity != null) && (rstatus.extHumidity.sensor1 != null)) {
updated |= updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_HUM, toQuantityType(
getDouble(rstatus.extHumidity.sensor1.hum), DIGITS_PERCENT, SmartHomeUnits.PERCENT));
}
// Update Auto-ON/OFF timer
ShellySettingsRelay rsettings = profile.settings.relays.get(i);
if (rsettings != null) {
updated |= updateChannel(groupName, CHANNEL_TIMER_AUTOON,
toQuantityType(getDouble(rsettings.autoOn), SmartHomeUnits.SECOND));
updated |= updateChannel(groupName, CHANNEL_TIMER_AUTOOFF,
toQuantityType(getDouble(rsettings.autoOff), SmartHomeUnits.SECOND));
}
// Update input(s) state
updated |= updateInputs(groupName, status, i);
}
i++;
}
}
// Check for Relay in Roller Mode
if (profile.hasRelays && profile.isRoller && (status.rollers != null)) {
logger.trace("{}: Updating {} rollers", thingName, profile.numRollers);
int i = 0;
for (ShellySettingsRoller roller : status.rollers) {
if (roller.isValid) {
ShellyControlRoller control = api.getRollerStatus(i);
Integer relayIndex = i + 1;
String groupName = profile.numRollers > 1 ? CHANNEL_GROUP_ROL_CONTROL + relayIndex.toString()
: CHANNEL_GROUP_ROL_CONTROL;
createRollerChannels(control);
if (control.name != null) {
updated |= updateChannel(groupName, CHANNEL_OUTPUT_NAME, getStringType(control.name));
}
String state = getString(control.state);
if (state.equals(SHELLY_ALWD_ROLLER_TURN_STOP)) { // only valid in stop state
int pos = Math.max(SHELLY_MIN_ROLLER_POS, Math.min(control.currentPos, SHELLY_MAX_ROLLER_POS));
updated |= updateChannel(groupName, CHANNEL_ROL_CONTROL_CONTROL,
toQuantityType(new Double(SHELLY_MAX_ROLLER_POS - pos), SmartHomeUnits.PERCENT));
updated |= updateChannel(groupName, CHANNEL_ROL_CONTROL_POS,
toQuantityType(new Double(pos), SmartHomeUnits.PERCENT));
scheduledUpdates = 1; // one more poll and then stop
}
updated |= updateChannel(groupName, CHANNEL_ROL_CONTROL_STATE, new StringType(state));
updated |= updateChannel(groupName, CHANNEL_ROL_CONTROL_STOPR, getStringType(control.stopReason));
updated |= updateInputs(groupName, status, i);
i++;
}
}
}
return updated;
}
/**
* Update Relay/Roller channels
*
* @param th Thing Handler instance
* @param profile ShellyDeviceProfile
* @param status Last ShellySettingsStatus
*
* @throws ShellyApiException
*/
public boolean updateDimmers(ShellySettingsStatus orgStatus) throws ShellyApiException {
boolean updated = false;
if (profile.isDimmer) {
// We need to fixup the returned Json: The dimmer returns light[] element, which is ok, but it doesn't have
// the same structure as lights[] from Bulb,RGBW2 and Duo. The tag gets replaced by dimmers[] so that Gson
// maps to a different structure (ShellyShortLight).
Gson gson = new Gson();
ShellySettingsStatus dstatus = gson.fromJson(ShellyApiJsonDTO.fixDimmerJson(orgStatus.json),
ShellySettingsStatus.class);
logger.trace("{}: Updating {} dimmers(s)", thingName, dstatus.dimmers.size());
int l = 0;
for (ShellyShortLightStatus dimmer : dstatus.dimmers) {
Integer r = l + 1;
String groupName = profile.numRelays <= 1 ? CHANNEL_GROUP_DIMMER_CONTROL
: CHANNEL_GROUP_DIMMER_CONTROL + r.toString();
// On a status update we map a dimmer.ison = false to brightness 0 rather than the device's brightness
// and send a OFF status to the same channel.
// When the device's brightness is > 0 we send the new value to the channel and a ON command
if (dimmer.ison) {
updated |= updateChannel(groupName, CHANNEL_BRIGHTNESS + "$Switch", OnOffType.ON);
updated |= updateChannel(groupName, CHANNEL_BRIGHTNESS + "$Value", toQuantityType(
new Double(getInteger(dimmer.brightness)), DIGITS_NONE, SmartHomeUnits.PERCENT));
} else {
updated |= updateChannel(groupName, CHANNEL_BRIGHTNESS + "$Switch", OnOffType.OFF);
updated |= updateChannel(groupName, CHANNEL_BRIGHTNESS + "$Value",
toQuantityType(new Double(0), DIGITS_NONE, SmartHomeUnits.PERCENT));
}
ShellySettingsDimmer dsettings = profile.settings.dimmers.get(l);
if (dsettings != null) {
updated |= updateChannel(groupName, CHANNEL_TIMER_AUTOON,
toQuantityType(getDouble(dsettings.autoOn), SmartHomeUnits.SECOND));
updated |= updateChannel(groupName, CHANNEL_TIMER_AUTOOFF,
toQuantityType(getDouble(dsettings.autoOff), SmartHomeUnits.SECOND));
}
updated |= updateInputs(groupName, orgStatus, l);
l++;
}
}
return updated;
}
/**
* Update LED channels
*
* @param th Thing Handler instance
* @param profile ShellyDeviceProfile
* @param status Last ShellySettingsStatus
*/
public boolean updateLed(ShellySettingsStatus status) {
boolean updated = false;
updated |= updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_LED_STATUS_DISABLE,
getOnOff(profile.settings.ledStatusDisable));
updated |= updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_LED_POWER_DISABLE,
getOnOff(profile.settings.ledPowerDisable));
return updated;
}
}

View File

@@ -0,0 +1,136 @@
/**
* Copyright (c) 2010-2020 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.shelly.internal.util;
import static org.openhab.binding.shelly.internal.util.ShellyUtils.mkChannelId;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.shelly.internal.handler.ShellyBaseHandler;
import org.openhab.core.types.State;
import org.openhab.core.types.UnDefType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link ShellyChannelCache} implements a caching layer for channel updates.
*
* @author Markus Michels - Initial contribution
*/
@NonNullByDefault
public class ShellyChannelCache {
private final Logger logger = LoggerFactory.getLogger(ShellyChannelCache.class);
private final ShellyBaseHandler thingHandler;
private final Map<String, State> channelData = new ConcurrentHashMap<>();
private String thingName = "";
private boolean enabled = false;
public ShellyChannelCache(ShellyBaseHandler thingHandler) {
this.thingHandler = thingHandler;
setThingName(thingHandler.thingName);
}
public void setThingName(String thingName) {
this.thingName = thingName;
}
public boolean isEnabled() {
return enabled;
}
public void enable() {
enabled = true;
}
public synchronized void disable() {
clear();
enabled = false;
}
/**
* Update one channel. Use Channel Cache to avoid unnecessary updates (and avoid
* messing up the log with those updates)
*
* @param channelId Channel id
* @param value Value (State)
* @param forceUpdate true: ignore cached data, force update; false check cache of changed data
* @return true, if successful
*/
public boolean updateChannel(String channelId, State newValue, Boolean forceUpdate) {
try {
State current = null;
if (channelData.containsKey(channelId)) {
current = channelData.get(channelId);
}
if (!enabled || forceUpdate || (current == null) || !current.equals(newValue)) {
if ((current != null) && current.getClass().isEnum() && (current == newValue)) {
return false; // special case for OnOffType
}
// For channels that support multiple types (like brightness) a suffix is added
// this gets removed to get the channelId for updateState
thingHandler.publishState(channelId, newValue);
if (current == null) {
channelData.put(channelId, newValue);
} else {
channelData.replace(channelId, newValue);
}
logger.debug("{}: Channel {} updated with {} (type {}).", thingName, channelId, newValue,
newValue.getClass());
return true;
}
} catch (IllegalArgumentException e) {
logger.debug("{}: Unable to update channel {} with {} (type {}): {} ({})", thingName, channelId, newValue,
newValue.getClass(), ShellyUtils.getMessage(e), e.getClass());
}
return false;
}
public boolean updateChannel(String group, String channel, State value) {
return updateChannel(mkChannelId(group, channel), value, false);
}
public boolean updateChannel(String channelId, State value) {
return updateChannel(channelId, value, false);
}
/**
* Get a value from the Channel Cache
*
* @param group Channel Group
* @param channel Channel Name
* @return the data from that channel
*/
public State getValue(String group, String channel) {
return getValue(mkChannelId(group, channel));
}
public State getValue(String channelId) {
if (channelData.containsKey(channelId)) {
return channelData.get(channelId);
}
return UnDefType.NULL;
}
public void resetChannel(String channelId) {
channelData.remove(channelId);
}
public void clear() {
channelData.clear();
}
}

View File

@@ -0,0 +1,63 @@
/**
* Copyright (c) 2010-2020 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.shelly.internal.util;
import java.util.Locale;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.i18n.LocaleProvider;
import org.openhab.core.i18n.TranslationProvider;
import org.osgi.framework.Bundle;
/**
* {@link ShellyTranslationProvider} provides i18n message lookup
*
* @author Markus Michels - Initial contribution
*/
@NonNullByDefault
public class ShellyTranslationProvider {
private final Bundle bundle;
private final TranslationProvider i18nProvider;
private final LocaleProvider localeProvider;
public ShellyTranslationProvider(Bundle bundle, TranslationProvider i18nProvider, LocaleProvider localeProvider) {
this.bundle = bundle;
this.i18nProvider = i18nProvider;
this.localeProvider = localeProvider;
}
public ShellyTranslationProvider(final ShellyTranslationProvider other) {
this.bundle = other.bundle;
this.i18nProvider = other.i18nProvider;
this.localeProvider = other.localeProvider;
}
public @Nullable String get(String key, @Nullable Object... arguments) {
return getText(key.contains("@text/") || key.contains(".shelly.") ? key : "message." + key, arguments);
}
public @Nullable String getText(String key, @Nullable Object... arguments) {
try {
Locale locale = localeProvider.getLocale();
return i18nProvider.getText(bundle, key, getDefaultText(key), locale, arguments);
} catch (IllegalArgumentException e) {
return "Unable to load message for key " + key;
}
}
public @Nullable String getDefaultText(String key) {
return i18nProvider.getText(bundle, key, key, Locale.ENGLISH);
}
}

View File

@@ -0,0 +1,255 @@
/**
* Copyright (c) 2010-2020 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.shelly.internal.util;
import static org.openhab.binding.shelly.internal.ShellyBindingConstants.*;
import java.io.UnsupportedEncodingException;
import java.math.BigDecimal;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.time.DateTimeException;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import javax.measure.Unit;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.shelly.internal.api.ShellyApiException;
import org.openhab.binding.shelly.internal.api.ShellyDeviceProfile;
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.PercentType;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.types.Command;
import org.openhab.core.types.State;
import org.openhab.core.types.UnDefType;
/**
* {@link ShellyUtils} provides general utility functions
*
* @author Markus Michels - Initial contribution
*/
@NonNullByDefault
public class ShellyUtils {
public static String mkChannelId(String group, String channel) {
return group + "#" + channel;
}
public static String getString(@Nullable String value) {
return value != null ? value : "";
}
public static String substringBefore(@Nullable String string, String pattern) {
if (string != null) {
int pos = string.indexOf(pattern);
if (pos > 0) {
return string.substring(0, pos);
}
}
return "";
}
public static String substringBeforeLast(@Nullable String string, String pattern) {
if (string != null) {
int pos = string.lastIndexOf(pattern);
if (pos > 0) {
return string.substring(0, pos);
}
}
return "";
}
public static String substringAfter(@Nullable String string, String pattern) {
if (string != null) {
int pos = string.indexOf(pattern);
if (pos != -1) {
return string.substring(pos + pattern.length());
}
}
return "";
}
public static String substringAfterLast(@Nullable String string, String pattern) {
if (string != null) {
int pos = string.lastIndexOf(pattern);
if (pos != -1) {
return string.substring(pos + pattern.length());
}
}
return "";
}
public static String substringBetween(@Nullable String string, String begin, String end) {
if (string != null) {
int s = string.indexOf(begin);
if (s != -1) {
// The end tag might be included before the start tag, e.g.
// when using "http://" and ":" to get the IP from http://192.168.1.1:8081/xxx
// therefore make it 2 steps
String result = string.substring(s + begin.length());
return substringBefore(result, end);
}
}
return "";
}
public static String getMessage(Exception e) {
String message = e.getMessage();
return message != null ? message : "";
}
public static Integer getInteger(@Nullable Integer value) {
return (value != null ? (Integer) value : 0);
}
public static Long getLong(@Nullable Long value) {
return (value != null ? (Long) value : 0);
}
public static Double getDouble(@Nullable Double value) {
return (value != null ? (Double) value : 0);
}
public static Boolean getBool(@Nullable Boolean value) {
return (value != null ? (Boolean) value : false);
}
// as State
public static StringType getStringType(@Nullable String value) {
return new StringType(value != null ? value : "");
}
public static DecimalType getDecimal(@Nullable Double value) {
return new DecimalType((value != null ? value : 0));
}
public static DecimalType getDecimal(@Nullable Integer value) {
return new DecimalType((value != null ? value : 0));
}
public static DecimalType getDecimal(@Nullable Long value) {
return new DecimalType((value != null ? value : 0));
}
public static Double getNumber(Command command) throws IllegalArgumentException {
if (command instanceof DecimalType) {
return ((DecimalType) command).doubleValue();
}
if (command instanceof QuantityType) {
return ((QuantityType<?>) command).doubleValue();
}
throw new IllegalArgumentException("Unable to convert number");
}
public static OnOffType getOnOff(@Nullable Boolean value) {
return (value != null ? value ? OnOffType.ON : OnOffType.OFF : OnOffType.OFF);
}
public static OnOffType getOnOff(int value) {
return value == 0 ? OnOffType.OFF : OnOffType.ON;
}
public static State toQuantityType(@Nullable Double value, int digits, Unit<?> unit) {
if (value == null) {
return UnDefType.NULL;
}
BigDecimal bd = new BigDecimal(value.doubleValue());
return toQuantityType(bd.setScale(digits, BigDecimal.ROUND_HALF_UP), unit);
}
public static State toQuantityType(@Nullable Number value, Unit<?> unit) {
return value == null ? UnDefType.NULL : new QuantityType<>(value, unit);
}
public static State toQuantityType(@Nullable PercentType value, Unit<?> unit) {
return value == null ? UnDefType.NULL : toQuantityType(value.toBigDecimal(), unit);
}
public static void validateRange(String name, Integer value, int min, int max) {
if ((value < min) || (value > max)) {
throw new IllegalArgumentException("Value " + name + " is out of range (" + min + "-" + max + ")");
}
}
public static String urlEncode(String input) throws ShellyApiException {
try {
return URLEncoder.encode(input, StandardCharsets.UTF_8.toString());
} catch (UnsupportedEncodingException e) {
throw new ShellyApiException(
"Unsupported encoding format: " + StandardCharsets.UTF_8.toString() + ", input=" + input, e);
}
}
public static Long now() {
return System.currentTimeMillis() / 1000L;
}
public static DateTimeType getTimestamp() {
return new DateTimeType(ZonedDateTime.ofInstant(Instant.ofEpochSecond(now()), ZoneId.systemDefault()));
}
public static DateTimeType getTimestamp(String zone, long timestamp) {
try {
if (timestamp == 0) {
return getTimestamp();
}
ZoneId zoneId = !zone.isEmpty() ? ZoneId.of(zone) : ZoneId.systemDefault();
ZonedDateTime zdt = LocalDateTime.now().atZone(zoneId);
int delta = zdt.getOffset().getTotalSeconds();
return new DateTimeType(ZonedDateTime.ofInstant(Instant.ofEpochSecond(timestamp - delta), zoneId));
} catch (DateTimeException e) {
// Unable to convert device's timezone, use system one
return getTimestamp();
}
}
public static Integer getLightIdFromGroup(String groupName) {
if (groupName.startsWith(CHANNEL_GROUP_LIGHT_CHANNEL)) {
return Integer.parseInt(substringAfter(groupName, CHANNEL_GROUP_LIGHT_CHANNEL)) - 1;
}
return 0; // only 1 light, e.g. bulb or rgbw2 in color mode
}
public static String buildControlGroupName(ShellyDeviceProfile profile, Integer channelId) {
return profile.isBulb || profile.isDuo || profile.inColor ? CHANNEL_GROUP_LIGHT_CONTROL
: CHANNEL_GROUP_LIGHT_CHANNEL + channelId.toString();
}
public static String buildWhiteGroupName(ShellyDeviceProfile profile, Integer channelId) {
return profile.isBulb || profile.isDuo && !profile.inColor ? CHANNEL_GROUP_WHITE_CONTROL
: CHANNEL_GROUP_LIGHT_CHANNEL + channelId.toString();
}
public static DecimalType mapSignalStrength(int dbm) {
int strength = -1;
if (dbm > -60) {
strength = 4;
} else if (dbm > -70) {
strength = 3;
} else if (dbm > -80) {
strength = 2;
} else if (dbm > -90) {
strength = 1;
} else {
strength = 0;
}
return new DecimalType(strength);
}
}

View File

@@ -0,0 +1,168 @@
/**
* Copyright (c) 2010-2020 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.shelly.internal.util;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* {@link ShellyVersionDTO} compares 2 version strings.
*
* @author Markus Michels - Initial contribution
*/
@NonNullByDefault
public class ShellyVersionDTO {
private class VersionTokenizer {
private final String versionString;
private final int length;
private int position;
private int number;
private String suffix = "";
public VersionTokenizer(@Nullable String versionString) {
if (versionString == null) {
throw new IllegalArgumentException("versionString is null");
}
this.versionString = versionString;
length = versionString.length();
}
private boolean moveNext() {
number = 0;
suffix = "";
// No more characters
if (position >= length) {
return false;
}
while (position < length) {
char c = versionString.charAt(position);
if (c < '0' || c > '9') {
break;
}
number = number * 10 + (c - '0');
position++;
}
int suffixStart = position;
while (position < length) {
char c = versionString.charAt(position);
if (c == '.') {
break;
}
position++;
}
suffix = versionString.substring(suffixStart, position);
if (position < length) {
position++;
}
return true;
}
private int getNumber() {
return number;
}
private String getSuffix() {
return suffix;
}
}
public boolean equals(String s1, String s2) {
return compare(s1, s2) == 0;
}
public int compare(String version1, String version2) {
VersionTokenizer tokenizer1 = new VersionTokenizer(version1);
VersionTokenizer tokenizer2 = new VersionTokenizer(version2);
int number1 = 0, number2 = 0;
String suffix1 = "", suffix2 = "";
while (tokenizer1.moveNext()) {
if (!tokenizer2.moveNext()) {
do {
number1 = tokenizer1.getNumber();
suffix1 = tokenizer1.getSuffix();
if (number1 != 0 || suffix1.length() != 0) {
// Version one is longer than number two, and non-zero
return 1;
}
} while (tokenizer1.moveNext());
// Version one is longer than version two, but zero
return 0;
}
number1 = tokenizer1.getNumber();
suffix1 = tokenizer1.getSuffix();
number2 = tokenizer2.getNumber();
suffix2 = tokenizer2.getSuffix();
if (number1 < number2) {
// Number one is less than number two
return -1;
}
if (number1 > number2) {
// Number one is greater than number two
return 1;
}
boolean empty1 = suffix1.length() == 0;
boolean empty2 = suffix2.length() == 0;
if (empty1 && empty2) {
continue;
} // No suffixes
if (empty1) {
return 1;
} // First suffix is empty (1.2 > 1.2b)
if (empty2) {
return -1;
} // Second suffix is empty (1.2a < 1.2)
// Lexical comparison of suffixes
int result = suffix1.compareTo(suffix2);
if (result != 0) {
return result;
}
}
while (tokenizer2.moveNext()) {
number2 = tokenizer2.getNumber();
suffix2 = tokenizer2.getSuffix();
if (number2 != 0 || suffix2.length() != 0) {
// Version one is longer than version two, and non-zero
return -1;
}
}
// Version two is longer than version one, but zero
return 0;
}
public boolean checkBeta(@Nullable String version) {
if (version == null) {
return false;
}
return version.isEmpty() || version.contains("???") || version.toLowerCase().contains("master")
|| (version.toLowerCase().contains("-rc"));
}
}

View File

@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<binding:binding id="shelly" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:binding="https://openhab.org/schemas/binding/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/binding/v1.0.0 https://openhab.org/schemas/binding-1.0.0.xsd">
<name>Shelly Binding</name>
<description>This binding supports the Shelly series of devices.</description>
<author>Markus Michels</author>
<config-description>
<parameter name="defaultUserId" type="text">
<default>admin</default>
<label>Default User</label>
<description>Default userId to access protected Shelly devices.</description>
</parameter>
<parameter name="defaultPassword" type="text">
<default>admin</default>
<label>Default Password</label>
<description>Default password to access protected Shelly devices.</description>
</parameter>
<parameter name="autoCoIoT" type="boolean">
<default>true</default>
<label>Auto-enable CoIoT</label>
<description>True: Enable CoIoT events by default when firmware 1.6+ is detected</description>
</parameter>
</config-description>
</binding:binding>

View File

@@ -0,0 +1,298 @@
<?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:shelly:relay">
<parameter name="userId" type="text" required="false">
<label>User</label>
<description>User ID for HTTP API access.</description>
</parameter>
<parameter name="password" type="text" required="false">
<label>Password</label>
<description>Password for HTTP API access.</description>
<context>password</context>
</parameter>
<parameter name="deviceIp" type="text" required="true">
<label>Device IP Address</label>
<description>IP-Address of the Shelly device.</description>
<context>network-address</context>
</parameter>
<parameter name="eventsButton" type="boolean" required="false">
<label>Enable Button Events</label>
<description>True if the binding should register to get the Buttons Events.</description>
<advanced>true</advanced>
<default>false</default>
</parameter>
<parameter name="eventsSwitch" type="boolean" required="false">
<label>Enable Switch Events (Out on/off)</label>
<description>True if the binding should register to get the Relay Switch Events.</description>
<advanced>true</advanced>
<default>false</default>
</parameter>
<parameter name="eventsPush" type="boolean" required="false">
<label>Enable Push Events (short/long)</label>
<description>True if the binding should register to get the Short/Long Push Events.</description>
<advanced>true</advanced>
<default>false</default>
</parameter>
<parameter name="eventsCoIoT" type="boolean" required="false">
<label>Enable CoIoT Events</label>
<description>If enabled, the CoAP protocol is used to receive status updates.</description>
<advanced>true</advanced>
<default>true</default>
</parameter>
<parameter name="updateInterval" type="integer" required="true" unit="s">
<label>Update Interval</label>
<description>Interval to query an update from the device.</description>
<default>60</default>
<unitLabel>seconds</unitLabel>
<advanced>true</advanced>
</parameter>
</config-description>
<config-description uri="thing-type:shelly:roller">
<parameter name="userId" type="text" required="false">
<label>User</label>
<description>User ID for HTTP API access.</description>
</parameter>
<parameter name="password" type="text" required="false">
<label>Password</label>
<description>Password for HTTP API access.</description>
<context>password</context>
</parameter>
<parameter name="deviceIp" type="text" required="true">
<label>Device IP Address</label>
<description>IP-Address of the Shelly device.</description>
<context>network-address</context>
</parameter>
<parameter name="eventsRoller" type="boolean" required="false">
<label>Enable Roller Events (Roller only)</label>
<description>True if the binding should register to get Roller Events.</description>
<advanced>true</advanced>
<default>true</default>
</parameter>
<parameter name="eventsCoIoT" type="boolean" required="false">
<label>Enable CoIoT Events</label>
<description>If enabled, the CoAP protocol is used to receive status updates.</description>
<advanced>true</advanced>
<default>true</default>
</parameter>
<parameter name="updateInterval" type="integer" required="true" unit="s">
<label>Update Interval</label>
<description>Interval to query an update from the device.</description>
<default>60</default>
<unitLabel>seconds</unitLabel>
<advanced>true</advanced>
</parameter>
</config-description>
<config-description uri="thing-type:shelly:dimmer">
<parameter name="userId" type="text" required="false">
<label>User</label>
<description>User ID for HTTP API access.</description>
</parameter>
<parameter name="password" type="text" required="false">
<label>Password</label>
<description>Password for HTTP API access.</description>
<context>password</context>
</parameter>
<parameter name="deviceIp" type="text" required="true">
<label>Device IP Address</label>
<description>IP-Address of the Shelly device.</description>
<context>network-address</context>
</parameter>
<parameter name="brightnessAutoOn" type="boolean" required="false">
<label>Brightness Auto-ON</label>
<description>true: Turn device ON if brightness>0 is set; false: don't touch power status when brightness is set.</description>
<default>true</default>
</parameter>
<parameter name="eventsButton" type="boolean" required="false">
<label>Enable Button Events</label>
<description>True if the binding should register to get the Buttons Events.</description>
<advanced>true</advanced>
<default>false</default>
</parameter>
<parameter name="eventsSwitch" type="boolean" required="false">
<label>Enable Switch Events (Out on/off)</label>
<description>True if the binding should register to get the Relay Switch Events.</description>
<advanced>true</advanced>
<default>false</default>
</parameter>
<parameter name="eventsPush" type="boolean" required="false">
<label>Enable Push Events (short/long)</label>
<description>True if the binding should register to get the Short/Long Push Events.</description>
<advanced>true</advanced>
<default>false</default>
</parameter>
<parameter name="eventsCoIoT" type="boolean" required="false">
<label>Enable CoIoT Events</label>
<description>If enabled, the CoAP protocol is used to receive status updates.</description>
<advanced>true</advanced>
<default>true</default>
</parameter>
<parameter name="updateInterval" type="integer" required="true" unit="s">
<label>Update Interval</label>
<description>Interval to query an update from the device.</description>
<default>60</default>
<unitLabel>seconds</unitLabel>
<advanced>true</advanced>
</parameter>
</config-description>
<config-description uri="thing-type:shelly:light">
<parameter name="userId" type="text" required="false">
<label>User</label>
<description>User ID for HTTP API access.</description>
</parameter>
<parameter name="password" type="text" required="false">
<label>Password</label>
<description>Password for HTTP API access.</description>
<context>password</context>
</parameter>
<parameter name="deviceIp" type="text" required="true">
<label>Device IP Address</label>
<description>IP-Address of the Shelly device.</description>
<context>network-address</context>
</parameter>
<parameter name="brightnessAutoOn" type="boolean" required="false">
<label>Brightness Auto-ON</label>
<description>true: Turn device ON if brightness > 0 is set; false: don't touch power status when brightness is set.</description>
<default>true</default>
</parameter>
<parameter name="eventsSwitch" type="boolean" required="false">
<label>Enable Switch Events (Out on/off)</label>
<description>True if the binding should register to get the Relay Switch Events.</description>
<advanced>true</advanced>
<default>false</default>
</parameter>
<parameter name="eventsCoIoT" type="boolean" required="false">
<label>Enable CoIoT Events</label>
<description>If enabled, the CoAP protocol is used to receive status updates.</description>
<advanced>true</advanced>
<default>true</default>
</parameter>
<parameter name="updateInterval" type="integer" required="true" unit="s">
<label>Update Interval</label>
<description>Interval to query an update from the device.</description>
<default>60</default>
<unitLabel>seconds</unitLabel>
<advanced>true</advanced>
</parameter>
</config-description>
<config-description uri="thing-type:shelly:rgbw2">
<parameter name="userId" type="text" required="false">
<label>User</label>
<description>User ID for HTTP API access.</description>
</parameter>
<parameter name="password" type="text" required="false">
<label>Password</label>
<description>Password for HTTP API access.</description>
<context>password</context>
</parameter>
<parameter name="deviceIp" type="text" required="true">
<label>Device IP Address</label>
<description>IP-Address of the Shelly device.</description>
<context>network-address</context>
</parameter>
<parameter name="brightnessAutoOn" type="boolean" required="false">
<label>Brightness Auto-ON</label>
<description>true: Turn device ON if brightness > 0 is set; false: don't touch power status when brightness is set.</description>
<default>true</default>
</parameter>
<parameter name="eventsCoIoT" type="boolean" required="false">
<label>Enable CoIoT Events</label>
<description>If enabled, the CoAP protocol is used to receive status updates.</description>
<advanced>true</advanced>
<default>true</default>
</parameter>
<parameter name="updateInterval" type="integer" required="true" unit="s">
<label>Update Interval</label>
<description>Interval to query an update from the device.</description>
<default>60</default>
<unitLabel>seconds</unitLabel>
<advanced>true</advanced>
</parameter>
</config-description>
<config-description uri="thing-type:shelly:battery">
<parameter name="userId" type="text" required="false">
<label>User ID</label>
<description>User ID for HTTP API access.</description>
</parameter>
<parameter name="password" type="text" required="false">
<label>Password</label>
<description>Password for HTTP API access.</description>
<context>password</context>
</parameter>
<parameter name="deviceIp" type="text" required="true">
<label>Device IP Address</label>
<description>IP-Address of the Shelly device.</description>
<context>network-address</context>
</parameter>
<parameter name="lowBattery" type="integer" required="false">
<label>Low Battery Threshold</label>
<description>Threshold for the battery level, alert will be signed when battery level is below. Default: 20%</description>
<default>20</default>
<unitLabel>%</unitLabel>
</parameter>
<parameter name="eventsSensorReport" type="boolean" required="false">
<label>Enable Sensor Events</label>
<description>True: Register event URL for sensor updates.</description>
<advanced>true</advanced>
<default>false</default>
</parameter>
<parameter name="eventsCoIoT" type="boolean" required="false">
<label>Enable CoIoT Events</label>
<description>If enabled, the CoAP protocol is used to receive status updates.</description>
<advanced>true</advanced>
<default>true</default>
</parameter>
<parameter name="updateInterval" type="integer" required="true" unit="s">
<label>Update Interval</label>
<description>Interval to query an update from the device.</description>
<default>3600</default>
<unitLabel>seconds</unitLabel>
<advanced>true</advanced>
</parameter>
</config-description>
<config-description uri="thing-type:shelly:basic">
<parameter name="userId" type="text" required="false">
<label>User ID</label>
<description>User ID for HTTP API access.</description>
</parameter>
<parameter name="password" type="text" required="false">
<label>Password</label>
<description>Password for HTTP API access.</description>
<context>password</context>
</parameter>
<parameter name="deviceIp" type="text" required="true">
<label>Device IP Address</label>
<description>IP-Address of the Shelly device.</description>
<context>network-address</context>
</parameter>
<parameter name="eventsSensorReport" type="boolean" required="false">
<label>Enable Sensor Events</label>
<description>True: Register event URL for sensor updates.</description>
<advanced>true</advanced>
<default>false</default>
</parameter>
<parameter name="eventsCoIoT" type="boolean" required="false">
<label>Enable CoIoT Events</label>
<description>If enabled, the CoAP protocol is used to receive status updates.</description>
<advanced>true</advanced>
<default>true</default>
</parameter>
<parameter name="updateInterval" type="integer" required="true" unit="s">
<label>Update Interval</label>
<description>Interval to query an update from the device.</description>
<default>3600</default>
<unitLabel>seconds</unitLabel>
<advanced>true</advanced>
</parameter>
</config-description>
</config-description:config-descriptions>

View File

@@ -0,0 +1,56 @@
discovery.failed# Config status messages
config-status.error.missing-device-ip=IP address of the Shelly device is missing.
# Thing status descriptions
offline.conf-error-no-credentials = Device is password protected, but no credentials have been configured.
offline.conf-error-access-denied = Access denied, check user id and password.
offline.conf-error-wrong-mode = Device is no longer in the configured device mode {0}, required {1}. Delete the thing and re-discover the device.
offline.status-error-timeout = Device is not reachable (API timeout).
offline.status-error-unexpected-api-result = An unexpected API response. Please verify the logfile to get more detailed information.
offline.status-error-watchdog = Device is not responding, seems to be unavailable.
offline.status-error-restarted = The device has restarted and will be re-initialized.
message.versioncheck.failed = Unable to check firmware version: {0}
message.versioncheck.beta = Device is running a Beta version: {0}/{1} ({2}),make sure this is newer than {3} release build.
message.versioncheck.tooold = WARNING: Firmware might be too old, installed: {0}/{1} ({2}), required minimal {3}.
message.versioncheck.update = INFO: New firmware available: current version: {0}, new version: {1}
message.versioncheck.autocoiot = INFO: Firmware is full-filling the minimum version to auto-enable CoIoT
message.roller.calibrating = Device is not calibrated, use Shelly App to perform initial roller calibration.
message.init.noipaddress = Unable to detect local IP address. Please make sure that IPv4 is enabled for this interface and check openHAB Network Configuration.
message.init.protected = Device is password protected, enter correct credentials in thing configuration.
message.command.failed = ERROR: Unable to process command {0} for channel {1}
message.command.init = Thing not yet initialized, command {0} triggers initialization
message.statusupdate.failed = Unable to update status
message.event.triggered = Event triggered: {0}
message.coap.init.failed = Unable to start CoIoT: {0}
message.discovery.disabled = Device is marked as non-discoverable -> skip
message.discovery.protected = Device {0} reported 'Access defined' (missing userid/password or incorrect).
message.discovery.failed = Device discovery of device with IP address {0} failed: {1}
# Device
channel-type.shelly.deviceName.label = Device Name
channel-type.shelly.deviceName.description = Symbolic Device Name as configured in the Shelly App.
# Relay, external sensors
channel-type.shelly.outputName.label = Output Name
channel-type.shelly.outputName.description = Output/Channel Name as configured in the Shelly App.
channel-type.shelly.temperature1.label = Temperature 1
channel-type.shelly.temperature1.description = Temperature of external Sensor #1
channel-type.shelly.temperature2.label = Temperature 2
channel-type.shelly.temperature2.description = Temperature of external Sensor #2
channel-type.shelly.temperature3.label = Temperature 3
channel-type.shelly.temperature3.description = Temperature of external Sensor #3
channel-type.shelly.humidity.label = Humidity
channel-type.shelly.humidity.description = Relative humidity (0..100%)
# Roller
channel-type.shelly.rollerState.label = State
channel-type.shelly.rollerState.description = State of the roller (open/closed/stopped).
# LED disable
channel-type.shelly.ledPowerDisable.label = Disable Power LED
channel-type.shelly.ledPowerDisable.description = ON: The power status LED will be decativated
channel-type.shelly.ledStatusDisable.label = Disable Status LED
channel-type.shelly.ledStatusDisable.description = ON: The WiFi status LED will be decativated

View File

@@ -0,0 +1,582 @@
# binding
binding.shelly.name = Shelly Binding
binding.shelly.description = Dieses Binding integriert Shelly-Komponenten, die über WiFi gesteuert werden können.
# Config status messages
config-status.error.missing-deviceip=Die IP-Adresse des Shelly Gerätes ist nicht konfiguriert.
# Thing status descriptions
offline.conf-error-no-credentials = Gerät ist passwortgeschützt, aber es sind keine Anmeldedaten konfiguriert.
offline.conf-error-access-denied = Der Zugriff wurde verweigert. überprüfen Sie die konfigurierten Zugangsdaten, oder setzen diese entsprechend (analog zur Shelly App).
offline.conf-error-wrong-mode = Das Gerät befindet sich nicht mehr in der erwarteten Betriebsart. Löschen Sie das Gerät und führen Sie eine erneute Geräteerkennung durch.
offline.status-error-timeout = Das Gerät ist nicht erreichbar (API Timeout).
offline.status-error-unexpected-api-result = Es trat ein unerwartetes Problem beim API-Zugriff auf. Überprüfen Sie die Logdatei für genauere Informationen.
offline.status-error-watchdog = Das Gerät antwortet nicht und ist vermutlich nicht mehr verfügbar.
offline.status-error-restarted = Das Gerät wurde neu gestartet und wird erneut initialisiert.
# Status error messages
config-status.error.missing-userid = Keine Benutzerkennung in der Thing Konfiguration
# General messages
message.versioncheck.failed = Firmware-Version konnte nicht geprüft werden: {0}
message.versioncheck.beta = Es wurde eine Betaversion erkannt: {0}/{1} ({2}), bitte sicherstellen, dass diese neuer ist als Version {3} (Release Build).
message.versioncheck.tooold = ACHTUNG: Eine alter Firmware wurde erkannt: {0}/{1} ({2}), minimal erforderlich {3}.
message.versioncheck.update = INFO: Eine neue Firmwareversion ist verfügbar, aktuell: {0}, neu: {1}
message.versioncheck.autocoiot = INFO: Die Firmware unterstützt die Anforderung, Auto-CoIoT wurde aktiviert.
message.init.noipaddress = Es konnte keine lokale IP-Adresse ermittelt werden. Bitte sicherstellen, dass IPv4 aktiviert ist und das richtige Interface in der openHAB Netzwerk-Konfiguration ausgewählt ist.
message.init.protected = Das Gerät ist passwortgeschützt, die Zugangsdaten müssen in der Thing Konfiguration hinterlegt werden.
message.command.failed = FEHLER: Der Befehl {0} für Kanal {1} kann nicht verarbeitet werden
message.command.init = Thing aktuell nicht initialisiert, der Befehl {0} führt zur Initialisierung
message.statusupdate.failed = Status konnte nicht aktualisiert werden
message.event.triggered = Event erzeugt: {0}
message.coap.init.failed = CoAP/CoIoT konnte nicht gestartet werden: {0}
message.discovery.disabled = Das Gerät ist als "nicht erkennen" markiert und wird nicht übernommen.
message.discovery.protected = Das Gerät mit der IP-Adresse {0} ist zugriffsgeschützt und keine Zugangsdaten konfiguriert.
message.discovery.failed = Erkennung des Gerätes mit der IP-Adresse {0} ist fehlgeschlagen
message.roller.calibrating = Das Gerät ist nicht kalibriert. Es ist eine Kalibrierung mit der Shelly App erforderlich.
# thing types
thing-type.shelly.shelly1.label = Shelly1 (SHSW-1)
thing-type.shelly.shelly1.description = Shelly 1 (1 Relay)
thing-type.shelly.shelly1pm.label = Shelly 1PM (SHSW-PM)
thing-type.shelly.shelly1pm.description = Shelly 1PM mit 1xRelais und Strommesser
thing-type.shelly.shellyem.label = Shelly EM (SHEM)
thing-type.shelly.shellyem.description = Shelly EM zur Energiemessung
thing-type.shelly.shellyem3.label = Shelly 3EM (SHEM-3)
thing-type.shelly.shellyem3.description = Shelly 3EM zur Energiemessung
thing-type.shelly.shelly2-relay.label = Shelly2 Relay (SHSW-21)
thing-type.shelly.shelly2-relay.description = Shelly2 im Relais-Modus (2xRelais, Strommesser)
thing-type.shelly.shelly2-roller.label = Shelly2 Relay (SHSW-21)
thing-type.shelly.shelly2-roller.description = Shelly2 im Rollladen-Modus (1xRollladen, Strommesser)
thing-type.shelly.shelly25-relay.label = Shelly2.5 Relay (SHSW-25)
thing-type.shelly.shelly25-relay.description = Shelly2.5 im Relais-Modus (2xRelais, 2 Strommesser)
thing-type.shelly.shelly25-roller.label = Shelly2.5 Roller (SHSW-25)
thing-type.shelly.shelly25-roller.description = Shelly2.5 im Rollladen-Modus (1xRollladen, Strommesser)
thing-type.shelly.shelly4pro.label = Shelly4 Pro Relay (SHSW-4)
thing-type.shelly.shelly4pro.description = Shelly 4 Pro mit 4 Relais und Strommessern
thing-type.shelly.shellyplug.label = Shelly Plug (SHPLG)
thing-type.shelly.shellyplug.description = Shelly Plug als schaltbare Steckdose
thing-type.shelly.shellyplugs.label = Shelly Plug-S (SHPLG-S)
thing-type.shelly.shellyplugs.description = Shelly Plug-S als schaltbare Steckdose
thing-type.shelly.shellydimmer.label = Shelly Dimmer (SHDM-1)
thing-type.shelly.shellydimmer.description = Shelly mit Dimmer-Funktion
thing-type.shelly.shellydimmer2.label = Shelly Dimmer (SHDM-2)
thing-type.shelly.shellydimmer2.description = Shelly mit Dimmer-Funktion, 2. Generation
thing-type.shelly.shellybutton1.label = Shelly Button 1 (SHBTN-1)
thing-type.shelly.shellybutton1.description = Shelly Button 1 (batteriebetrieben)
thing-type.shelly.shellyht.label = Shelly H&T (SHHT-1)
thing-type.shelly.shellyht.description = Shelly H&amp;T Sensor
thing-type.shelly.shellydw.label = Shelly Door/Window Sensor (SHDW-1)
thing-type.shelly.shellydw.description = Shelly Tür/Fenstersensor (batteriebetrieben)
thing-type.shelly.shellydw2.label = Shelly Door/Window Sensor 2 (SHDW-2)
thing-type.shelly.shellydw2.description = Shelly Tür/Fenstersensor (batteriebetrieben)
thing-type.shelly.shellysmoke.label = Shelly Smoke
thing-type.shelly.shellysmoke.description = Shelly Rauchmelder
thing-type.shelly.shellyflood.label = Shelly Flood (SHWT-1)
thing-type.shelly.shellyflood.description = Shelly Wassermelder
thing-type.shelly.shellysense.label = Shelly Sense (SHSEN-1)
thing-type.shelly.shellysense.description = Shelly Bewegungsmelder
thing-type.shelly.shellybulb.label = Shelly Bulb (SHBLB-1)
thing-type.shelly.shellybulb.description = Shelly Glühbirne weiß/Farbe
thing-type.shelly.shellybulbduo.label = Shelly Duo (SHBDUO-1)
thing-type.shelly.shellybulbduo.description = Shelly Duo Glühbirne
thing-type.shelly.shellyvintage.label = Shelly Vintage (SHVIN-1)
thing-type.shelly.shellyvintage.description = Shelly Vintage Glühbirne
thing-type.shelly.shellyrgbw2-color.label = Shelly RGBW2 Color Mode (SHRGBW2)
thing-type.shelly.shellyrgbw2-color.description = Shelly RGBW-Controller im Farbmodus
thing-type.shelly.shellyrgbw2-white.label = Shelly RGBW2 White Mode (SHRGBW2)
thing-type.shelly.shellyrgbw2-white.description = Shelly RGBW-Controller im Weiß-Modus, 4 Streifen
# thing config - generic
thing-type.config.shelly.generic.userId.label = Benutzer
thing-type.config.shelly.generic.userId.description = Benutzerkennung für API-Zugriff
thing-type.config.shelly.generic.password.label = Passwort
thing-type.config.shelly.generic.password.description = Passwort für API-Zugriff
thing-type.config.shelly.generic.deviceIp.label = IP Adresse
thing-type.config.shelly.generic.deviceIp.description = IP Adresse der Shelly-Komponente
thing-type.config.shelly.generic.weakSignal.label = Schwaches Signal (dBm)
thing-type.config.shelly.generic.weakSignal.description = Ein Alarm wird ausgelöst, wenn das WiFi-Signal diesen Wert unterschreitet. Voreinstellung: -80 dBm
thing-type.config.shelly.generic.eventsButton.label = Button Events
thing-type.config.shelly.generic.eventsButton.description = Aktiviert die Button Action URLS
thing-type.config.shelly.generic.eventsPush.label = Push Events
thing-type.config.shelly.generic.eventsPush.description = Aktiviert die Push Button Action URLS
thing-type.config.shelly.generic.eventsSwitch.label = Output Events
thing-type.config.shelly.generic.eventsSwitch.description = Aktiviert die Output Action URLS
thing-type.config.shelly.generic.eventsCoIoT.label = CoIoT aktivieren
thing-type.config.shelly.generic.eventsCoIoT.description = Aktiviert CoIoT-Protokoll (CoAP-basiert)
thing-type.config.shelly.generic.updateInterval.label = Status-Intervall
thing-type.config.shelly.generic.updateInterval.description = Intervall für die Hintergundaktualisierung
# thing config - light
thing-type.config.shelly.light.userId.label = Benutzer
thing-type.config.shelly.light.userId.description = Benutzerkennung für API-Zugriff
thing-type.config.shelly.light.password.label = Passwort
thing-type.config.shelly.light.password.description = Passwort für API-Zugriff
thing-type.config.shelly.light.deviceIp.label = IP Adresse
thing-type.config.shelly.light.deviceIp.description = IP Adresse der Shelly-Komponente
thing-type.config.shelly.light.weakSignal.label = Schwaches Signal (dBm)
thing-type.config.shelly.light.weakSignal.description = Ein Alarm wird ausgelöst, wenn das WiFi-Signal diesen Wert unterschreitet. Voreinstellung: -80 dBm
thing-type.config.shelly.light.brightnessAutoOn.label = Helligkeit Auto-EIN
thing-type.config.shelly.light.brightnessAutoOn.description = AN: Setzen einer Helligkeit > 0 schaltet das Gerät automatisch ein; AUS: Gerätestatus wird nicht ge#ndert
thing-type.config.shelly.light.eventsButton.label = Button Events
thing-type.config.shelly.light.eventsButton.description = Aktiviert die Button Action URLS
thing-type.config.shelly.light.eventsPush.label = Push Events
thing-type.config.shelly.light.eventsPush.description = Aktiviert die Push Button Action URLS
thing-type.config.shelly.light.eventsSwitch.label = Output Events
thing-type.config.shelly.light.eventsSwitch.description = Aktiviert die Output Action URLS
thing-type.config.shelly.light.eventsCoIoT.label = CoIoT aktivieren
thing-type.config.shelly.light.eventsCoIoT.description = Aktiviert CoIoT-Protokoll (CoAP-basiert)
thing-type.config.shelly.light.updateInterval.label = Status-Intervall
thing-type.config.shelly.light.updateInterval.description = Intervall für die Hintergrundaktualisierung
# thing config - battery
thing-type.config.shelly.battery.userId.label = Benutzer
thing-type.config.shelly.battery.userId.description = Benutzerkennung für API-Zugriff
thing-type.config.shelly.battery.password.label = Passwort
thing-type.config.shelly.battery.password.description = Passwort für API-Zugriff
thing-type.config.shelly.battery.deviceIp.label = IP Adresse
thing-type.config.shelly.battery.deviceIp.description = IP Adresse der Shelly-Komponente
thing-type.config.shelly.battery.eventsSensorReport.label = Sensor Events
thing-type.config.shelly.battery.eventsSensorReport.description = Aktiviert die Sensor Action URLS
thing-type.config.shelly.battery.eventsCoIoT.label = CoIoT aktivieren
thing-type.config.shelly.battery.eventsCoIoT.description = Aktiviert CoIoT-Protokoll (CoAP-basiert)
thing-type.config.shelly.battery.weakSignal.label = Schwaches Signal (dBm)
thing-type.config.shelly.battery.weakSignal.description = Ein Alarm wird ausgelöst, wenn das WiFi-Signal diesen Wert unterschreitet. Voreinstellung: -80 dBm
thing-type.config.shelly.battery.lowBattery.label = Batterieladung niedrig (%)
thing-type.config.shelly.battery.lowBattery.description = Ein Alarm wird ausgelöst, wenn das Gerät eine Batterieladung kleiner diesem Schwellwert meldet. Default: 20%
thing-type.config.shelly.battery.updateInterval.label = Status-Intervall
thing-type.config.shelly.battery.updateInterval.description = Intervall für die Hintergundaktualisierung
# thing config - unknown
thing-type.config.shellydevice.userId.label = Benutzer
thing-type.config.shellydevice.userId.description = Benutzerkennung für API-Zugriff
thing-type.config.shellydevice.password.label = Passwort
thing-type.config.shellydevice.password.description = Passwort für API-Zugriff
thing-type.config.shellydevice.deviceIp.label = IP Adresse
thing-type.config.shellydevice.deviceIp.description = IP Adresse der Shelly-Komponente
# channel-groups
thing-type.shelly.shelly1.group.relay.label = Relais
thing-type.shelly.shelly1.group.relay.description = Relais Ein-/Ausgänge und Status
thing-type.shelly.shelly1.group.sensors.label = Externe Sensoren
thing-type.shelly.shelly1.group.sensors.description = Temperaturwerte der externen Sensoren (nur wenn angeschlossen)
thing-type.shelly.shelly1.group.device.label = Gerätestatus
thing-type.shelly.shelly1.group.device.description = Informationen zum Gerätestatus
thing-type.shelly.shelly1pm.group.relay.label = Relais
thing-type.shelly.shelly1pm.group.relay.description = Relais Ein-/Ausgänge und Status
thing-type.shelly.shelly1pm.group.meter.label = Verbrauch
thing-type.shelly.shelly1pm.group.meter.description = Verbrauchswerte und andere Informationen
thing-type.shelly.shelly1pm.group.sensors.label = Externe Sensoren
thing-type.shelly.shelly1pm.group.sensors.description = Werte der externen Sensoren (nur wenn angeschlossen)
thing-type.shelly.shelly1pm.group.device.label = Gerätestatus
thing-type.shelly.shelly1pm.group.device.description = Informationen zum Gerätestatus
thing-type.shelly.shellyem.group.relay.label = Relais
thing-type.shelly.shellyem.group.relay.description = Relais Ein-/Ausgänge und Status
thing-type.shelly.shellyem.group.meter1.label = Verbrauch 1
thing-type.shelly.shellyem.group.meter1.description = Verbrauchswerte und andere Informationen
thing-type.shelly.shellyem.group.meter2.label = Verbrauch 2
thing-type.shelly.shellyem.group.meter2.description = Verbrauchswerte und andere Informationen
thing-type.shelly.shellyem.group.device.label = Gerätestatus
thing-type.shelly.shellyem.group.device.description = Informationen zum Gerätestatus
thing-type.shelly.shellyem3.group.relay.label = Relais
thing-type.shelly.shellyem3.group.relay.description = Relais Ein-/Ausgänge und Status
thing-type.shelly.shellyem3.group.meter1.label = Verbrauch 1
thing-type.shelly.shellyem3.group.meter1.description = Verbrauchswerte und andere Informationen
thing-type.shelly.shellyem3.group.meter2.label = Verbrauch 2
thing-type.shelly.shellyem3.group.meter2.description = Verbrauchswerte und andere Informationen
thing-type.shelly.shellyem3.group.meter3.label = Verbrauch 3
thing-type.shelly.shellyem3.group.meter3.description = Verbrauchswerte und andere Informationen
thing-type.shelly.shellyem3.group.device.label = Gerätestatus
thing-type.shelly.shellyem3.group.device.description = Informationen zum Gerätestatus
thing-type.shelly.shelly2-relay.group.relay1.label = Relais 1
thing-type.shelly.shelly2-relay.group.relay1.description = Relais Ein-/Ausgänge und Status
thing-type.shelly.shelly2-relay.group.relay2.label = Relais 2
thing-type.shelly.shelly2-relay.group.relay2.description = Relais Ein-/Ausgänge und Status
thing-type.shelly.shelly2-relay.group.meter.label = Verbrauch
thing-type.shelly.shelly2-relay.group.meter.description = Verbrauchswerte und andere Informationen
thing-type.shelly.shelly2-relay.group.device.label = Gerätestatus
thing-type.shelly.shelly2-relay.group.device.description = Informationen zum Gerätestatus
thing-type.shelly.shelly2-roller.group.roller.label = Rollladen
thing-type.shelly.shelly2-roller.group.roller.description = Rollladensteuerung und Status
thing-type.shelly.shelly2-roller.group.meter.label = Verbrauch
thing-type.shelly.shelly2-roller.group.meter.description = Verbrauchswerte und andere Informationen
thing-type.shelly.shelly2-roller.group.device.label = Gerätestatus
thing-type.shelly.shelly2-roller.group.device.description = Informationen zum Gerätestatus
thing-type.shelly.shelly25-relay.group.relay1.label = Relais 1
thing-type.shelly.shelly25-relay.group.relay1.description = Relais Ein-/Ausgänge und -Funktionen
thing-type.shelly.shelly25-relay.group.relay2.label = Relais 2
thing-type.shelly.shelly25-relay.group.relay2.description = Relais Ein-/Ausgänge und -Funktionen
thing-type.shelly.shelly25-relay.group.meter1.label = Verbrauch 1
thing-type.shelly.shelly25-relay.group.meter1.description = Verbrauchswerte und andere Informationen
thing-type.shelly.shelly25-relay.group.meter2.label = Verbrauch 2
thing-type.shelly.shelly25-relay.group.meter2.description = Verbrauchswerte und andere Informationen
thing-type.shelly.shelly25-relay.group.device.label = Gerätestatus
thing-type.shelly.shelly25-relay.group.device.description = Informationen zum Gerätestatus
thing-type.shelly.shelly25-roller.group.roller.label = Rollladen
thing-type.shelly.shelly25-roller.group.roller.description = Rollladensteuerung und Status
thing-type.shelly.shelly25-roller.group.meter.label = Verbrauch
thing-type.shelly.shelly25-roller.group.meter.description = Verbrauchswerte und andere Informationen
thing-type.shelly.shelly25-roller.group.device.label = Gerätestatus
thing-type.shelly.shelly25-roller.group.device.description = Informationen zum Gerätestatus
thing-type.shelly.shelly4pro.group.relay1.label = Relais 1
thing-type.shelly.shelly4pro.group.relay1.description = Relais Ein-/Ausgänge und Status
thing-type.shelly.shelly4pro.group.relay2.label = Relais 2
thing-type.shelly.shelly4pro.group.relay2.description = Relais Ein-/Ausgänge und Status
thing-type.shelly.shelly4pro.group.relay3.label = Relais 3
thing-type.shelly.shelly4pro.group.relay3.description = Relais Ein-/Ausgänge und Status
thing-type.shelly.shelly4pro.group.relay4.label = Relais 4
thing-type.shelly.shelly4pro.group.relay4.description = Relais Ein-/Ausgänge und Status
thing-type.shelly.shelly4pro.group.meter1.label = Verbrauch 1
thing-type.shelly.shelly4pro.group.meter1.description = Verbrauchswerte und andere Informationen
thing-type.shelly.shelly4pro.group.meter2.label = Verbrauch 2
thing-type.shelly.shelly4pro.group.meter2.description = Verbrauchswerte und andere Informationen
thing-type.shelly.shelly4pro.group.meter3.label = Verbrauch 3
thing-type.shelly.shelly4pro.group.meter3.description = Verbrauchswerte und andere Informationen
thing-type.shelly.shelly4pro.group.meter4.label = Verbrauch 4
thing-type.shelly.shelly4pro.group.meter4.description = Verbrauchswerte und andere Informationen
thing-type.shelly.shelly4pro.group.device.label = Gerätestatus
thing-type.shelly.shelly4pro.group.device.description = Informationen zum Gerätestatus
thing-type.shelly.shellyplug.group.relay.label = Relais
thing-type.shelly.shellyplug.group.relay.description = Relais Ein-/Ausgänge und Status
thing-type.shelly.shellyplug.group.meter.label = Verbrauch
thing-type.shelly.shellyplug.group.meter.description = Verbrauchswerte und andere Informationen
thing-type.shelly.shellyplug.group.device.label = Gerätestatus
thing-type.shelly.shellyplug.group.device.description = Informationen zum Gerätestatus
thing-type.shelly.shellyplugs.group.relay.label = Relais
thing-type.shelly.shellyplugs.group.relay.description = Relais Ein-/Ausgänge und Status
thing-type.shelly.shellyplugs.group.meter.label = Verbrauch
thing-type.shelly.shellyplugs.group.meter.description = Verbrauchswerte und andere Informationen
thing-type.shelly.shellyplugs.group.device.label = Gerätestatus
thing-type.shelly.shellyplugs.group.device.description = Informationen zum Gerätestatus
thing-type.shelly.shellydimmer.group.relay.label = Relais
thing-type.shelly.shellydimmer.group.relay.description = Relais Ein-/Ausgänge und Status
thing-type.shelly.shellydimmer.group.meter.label = Verbrauch
thing-type.shelly.shellydimmer.group.meter.description = Verbrauchswerte und andere Informationen
thing-type.shelly.shellydimmer.group.device.label = Gerätestatus
thing-type.shelly.shellydimmer.group.device.description = Informationen zum Gerätestatus
thing-type.shelly.shellydimmer2.group.relay.label = Relais
thing-type.shelly.shellydimmer2.group.relay.description = Relais Ein-/Ausgänge und Status
thing-type.shelly.shellydimmer2.group.meter.label = Verbrauch
thing-type.shelly.shellydimmer2.group.meter.description = Verbrauchswerte und andere Informationen
thing-type.shelly.shellydimmer2.group.device.label = Gerätestatus
thing-type.shelly.shellydimmer2.group.device.description = Informationen zum Gerätestatus
thing-type.shelly.shellyix3.group.status1.label = Eingang #1
thing-type.shelly.shellyix3.group.status1.description = Status Informationen zum Eingang 1
thing-type.shelly.shellyix3.group.status2.label = Eingang #2
thing-type.shelly.shellyix3.group.status2.description = Status Informationen zum Eingang 2
thing-type.shelly.shellyix3.group.status3.label = Eingang #3
thing-type.shelly.shellyix3.group.status3.description = Status Informationen zum Eingang 3
thing-type.shelly.shellyix3.group.device.label = Gerätestatus
thing-type.shelly.shellyix3.group.device.description = Informationen zum Gerätestatus
thing-type.shelly.shellybutton1.group.status.label = Taster-Status
thing-type.shelly.shellybutton1.group.status.description = Informationen zum Gerätestatus
thing-type.shelly.shellybutton1.group.battery.label = Batteriestatus
thing-type.shelly.shellybutton1.group.battery.description = Informationen zum Akku
thing-type.shelly.shellybutton1.group.device.label = Gerätestatus
thing-type.shelly.shellybutton1.group.device.description = Informationen zum Gerätestatus
thing-type.shelly.shellybulb.group.control.label = Steuerung
thing-type.shelly.shellybulb.group.control.description = Steuerung des Lichts
thing-type.shelly.shellybulb.group.color.label = Farbmodus
thing-type.shelly.shellybulb.group.color.description = Einstellungen für den Farbmodus
thing-type.shelly.shellybulb.group.white.label = Weißwerte
thing-type.shelly.shellybulb.group.white.description = Einstellungen für den Weiß-Modus
thing-type.shelly.shellybulb.group.device.label = Gerätestatus
thing-type.shelly.shellybulb.group.device.description = Informationen zum Gerätestatus
thing-type.shelly.shellybulbduo.group.control.label = Steuerung
thing-type.shelly.shellybulbduo.group.control.description = Steuerung des Lichts
thing-type.shelly.shellybulbduo.group.white.label = Weißwerte
thing-type.shelly.shellybulbduo.group.white.description = Einstellungen für den Weiß-Modus
thing-type.shelly.shellybulbduo.group.meter.label = Verbrauch
thing-type.shelly.shellybulbduo.group.meter.description = Verbrauchswerte
thing-type.shelly.shellybulbduo.group.device.label = Gerätestatus
thing-type.shelly.shellybulbduo.group.device.description = Informationen zum Gerätestatus
thing-type.shelly.shellyvintage.group.control.label = Steuerung
thing-type.shelly.shellyvintage.group.control.description = Steuerung des Lichts
thing-type.shelly.shellyvintage.group.white.label = Weißwerte
thing-type.shelly.shellyvintage.group.white.description = Einstellungen für den Weiß-Modus
thing-type.shelly.shellyvintage.group.meter.label = Verbrauch
thing-type.shelly.shellyvintage.group.meter.description = Verbrauchswerte
thing-type.shelly.shellyvintage.group.device.label = Gerätestatus
thing-type.shelly.shellyvintage.group.device.description = Informationen zum Gerätestatus
thing-type.shelly.shellyrgbw2-color.group.control.label = Steuerung
thing-type.shelly.shellyrgbw2-color.group.control.description = Steuerung des Controllers
thing-type.shelly.shellyrgbw2-color.group.color.label = Farben
thing-type.shelly.shellyrgbw2-color.group.color.description = Farbwerte und Profile
thing-type.shelly.shellyrgbw2-color.group.meter.label = Verbrauch
thing-type.shelly.shellyrgbw2-color.group.meter.description = Verbrauchswerte und andere Informationen
thing-type.shelly.shellyrgbw2-color.group.device.label = Gerätestatus
thing-type.shelly.shellyrgbw2-color.group.device.description = Informationen zum Gerätestatus
thing-type.shelly.shellyrgbw2-white.group.control.label = Steuerung
thing-type.shelly.shellyrgbw2-white.group.control.description = Lichtsteuerung
thing-type.shelly.shellyrgbw2-white.group.channel1.label = Kanal 1
thing-type.shelly.shellyrgbw2-white.group.channel1.description = Steuerung für Kanal 1
thing-type.shelly.shellyrgbw2-white.group.channel2.label = Kanal 2
thing-type.shelly.shellyrgbw2-white.group.channel2.description = Steuerung für Kanal 2
thing-type.shelly.shellyrgbw2-white.group.channel3.label = Kanal 3
thing-type.shelly.shellyrgbw2-white.group.channel3.description = Steuerung für Kanal 3
thing-type.shelly.shellyrgbw2-white.group.channel4.label = Kanal 4
thing-type.shelly.shellyrgbw2-white.group.channel4.description = Steuerung für Kanal 4
thing-type.shelly.shellyrgbw2-white.group.meter.label = Verbrauch
thing-type.shelly.shellyrgbw2-white.group.meter.description = Verbrauchswerte und andere Informationen
thing-type.shelly.shellyrgbw2-white.group.device.label = Gerätestatus
thing-type.shelly.shellyrgbw2-white.group.device.description = Informationen zum Gerätestatus
thing-type.shelly.shellyht.group.sensors.label = Sensorwerte
thing-type.shelly.shellyht.group.sensors.description = Messwerte und Status des Sensors
thing-type.shelly.shellyht.group.battery.label = Batterie-Status
thing-type.shelly.shellyht.group.battery.description = Informationen zum Akku
thing-type.shelly.shellyht.group.device.label = Gerätestatus
thing-type.shelly.shellyht.group.device.description = Informationen zum Gerätestatus
thing-type.shelly.shellyflood.group.sensors.label = Sensorwerte
thing-type.shelly.shellyflood.group.sensors.description = Messwerte und Status des Sensors
thing-type.shelly.shellyflood.group.battery.label = Batterie-Status
thing-type.shelly.shellyflood.group.battery.description = Informationen zum Akku
thing-type.shelly.shellyflood.group.device.label = Gerätestatus
thing-type.shelly.shellyflood.group.device.description = Informationen zum Gerätestatus
thing-type.shelly.shellydw.group.sensors.label = Sensordaten
thing-type.shelly.shellydw.group.sensors.description = Messwerte und Status des Sensors
thing-type.shelly.shellydw.group.battery.label = Batterie-Status
thing-type.shelly.shellydw.group.battery.description = Informationen zum Akku
thing-type.shelly.shellydw.group.device.label = Gerätestatus
thing-type.shelly.shellydw.group.device.description = Informationen zum Gerätestatus
thing-type.shelly.shellydw2.group.sensors.label = Sensordaten
thing-type.shelly.shellydw2.group.sensors.description = Messwerte und Status des Sensors
thing-type.shelly.shellydw2.group.battery.label = Batterie-Status
thing-type.shelly.shellydw2.group.battery.description = Informationen zum Akku
thing-type.shelly.shellydw2.group.device.label = Gerätestatus
thing-type.shelly.shellydw2.group.device.description = Informationen zum Gerätestatus
thing-type.shelly.shellysmoke.sensors.device.label = Sensordaten
thing-type.shelly.shellysmoke.sensors.device.description = Messwerte und Status des Sensors
thing-type.shelly.shellysmoke.group.battery.label = Batterie-Status
thing-type.shelly.shellysmoke.group.battery.description = Informationen zum Akku
thing-type.shelly.shellysmoke.group.device.label = Gerätestatus
thing-type.shelly.shellysmoke.group.device.description = Informationen zum Gerätestatus
thing-type.shelly.shellysense.sensors.device.label = Sensordaten
thing-type.shelly.shellysense.sensors.device.description = Messwerte und Status des Sensors
thing-type.shelly.shellysense.group.device.label = Gerätestatus
thing-type.shelly.shellysense.group.device.description = Informationen zum Gerätestatus
thing-type.shelly.shellygas.group.sensors.label = Sensordaten
thing-type.shelly.shellygas.group.sensors.description = Messwerte und Status des Sensors
thing-type.shelly.shellygas.group.device.label = Gerätestatus
thing-type.shelly.shellygas.group.device.description = Informationen zum Gerätestatus
# channels
channel-type.shelly.outputName.label = Ausgangsname
channel-type.shelly.outputName.description = Name des Relais-Ausgangs/Kanals (Konfiguration innerhalb der Shelly App)
channel-type.shelly.timerAutoOn.label = Auto-EIN Timer
channel-type.shelly.timerAutoOn.description = Wenn das Relais ausgeschaltet wird, wird dieses automatisch wieder nach n Sekunden eingeschaltet
channel-type.shelly.timerAutoOff.label = Auto-AUS Timer
channel-type.shelly.timerAutoOff.description = Wenn das Relais eingeschaltet wird, wird dieses automatisch wieder nach n Sekunden ausgeschaltet
channel-type.shelly.timerActive.label = Timer aktiv
channel-type.shelly.timerActive.description = ON: Auto-On/Off Timer ist aktiv
channel-type.shelly.temperature1.label = Temperatur 1
channel-type.shelly.temperature1.description = Temperatur des externen Sensors #1
channel-type.shelly.temperature2.label = Temperatur 2
channel-type.shelly.temperature2.description = Temperatur des externen Sensors #2
channel-type.shelly.temperature3.label = Temperatur 3
channel-type.shelly.temperature3.description = Temperatur des externen Sensors #3
channel-type.shelly.humidity.label = Luftfeuchtigkeit
channel-type.shelly.humidity.description = Relative Luftfeuchtigkeit (0..100%)
channel-type.shelly.rollerShutter.label = Steuerung (0=offen, 100=geschlossen)
channel-type.shelly.rollerShutter.description = Steuerung für den Rollladen: UP, DOWN, STOP, Position (0=offen, 100=geschlossen)
channel-type.shelly.rollerPosition.label = Position (100=offen, 0=zu)
channel-type.shelly.rollerPosition.description = Invertierte Position des Rollladen: 100=offen, 0=zu
channel-type.shelly.rollerState.label = Status
channel-type.shelly.rollerState.description = Zustand des Rollladen (open/closed/stopped).
channel-type.shelly.rollerState.state.option.open = öffnet
channel-type.shelly.rollerState.state.option.close = schließt
channel-type.shelly.rollerState.state.option.stop = gestoppt
channel-type.shelly.rollerStop.label = Stoppgrund
channel-type.shelly.rollerStop.description = Letzter Grund für das Stoppen des Rollladens (normal=normaler Stopp, safety_switch=Sicherheits-Stopp, obstacle=Rollladen verhakt (Widerstand erkannt)
channel-type.shelly.inputState.label = Eingang
channel-type.shelly.inputState.description = Status des Relais-Eingangs
channel-type.shelly.inputState1.label = Eingang 1
channel-type.shelly.inputState1.description = Status des Relais-Eingangs 1
channel-type.shelly.inputState2.label = Eingang 2
channel-type.shelly.inputState2.description = Status des Relais-Eingangs 2
channel-type.shelly.inputState3.label = Eingang 3
channel-type.shelly.inputState3.description = Status des Relais-Eingangs 3
channel-type.shelly.dimmerBrightness.label = Helligkeit
channel-type.shelly.dimmerBrightness.description = Helligkeit (0-100%, 0=aus)
channel-type.shelly.whiteBrightness.label = Helligkeit
channel-type.shelly.whiteBrightness.description = Helligkeit (0-100%, 0=aus)
channel-type.shelly.meterWatts.label = Leistung
channel-type.shelly.meterWatts.description = Aktueller Stromverbrauch in Watt
channel-type.shelly.meterAccuWatts.label = Kumulierter Verbrauch
channel-type.shelly.meterAccuWatts.description = Kumulierter Verbrauch in Watt
channel-type.shelly.meterAccuTotal.label = Kumulierter Gesamtverbrauch
channel-type.shelly.meterAccuTotal.description = Kumulierter Gesamtverbrauch in kW/h
channel-type.shelly.meterAccuReturned.label = Kumulierter Einspeisung
channel-type.shelly.meterAccuReturned.description = Kumulierter Einspeisung in kW/h
channel-type.shelly.meterCurrent.label = Stromstärke
channel-type.shelly.meterCurrent.description = Aktuelle gemessene Stromstärke
channel-type.shelly.meterTotal.label = Gesamtverbrauch
channel-type.shelly.meterTotal.description = Gesamtverbrauch seit Neustart in kW/h
channel-type.shelly.meterReturned.label = Einspeisung
channel-type.shelly.meterReturned.description = Einspeisung in kW/h
channel-type.shelly.meterVoltage.label = Spannung
channel-type.shelly.meterVoltage.description = Spannung in Volt
channel-type.shelly.meterPowerFactor.label = Stromfaktor
channel-type.shelly.meterPowerFactor.description = Faktor für Strom bei Photovoltaik
channel-type.shelly.meterReactive.label = Rückstrom
channel-type.shelly.meterReactive.description = Aktueller Stromverbrauch (Watt) des Rückstroms
channel-type.shelly.lastPower1.label = Schnitt letzte Min
channel-type.shelly.lastPower1.description = Durchschnittsverbrauch der letzten Minute, 60s in Watt/Min
channel-type.shelly.timestamp.label = Letzte Aktualisierung
channel-type.shelly.timestamp.description = Zeitstempel der letzten Aktualisierung (lokale Zeitzone)
channel-type.shelly.ledStatusDisable.label = Status-LED aus
channel-type.shelly.ledStatusDisable.description = ON: Die Status-LED am Gerät ist deaktiviert
channel-type.shelly.ledPowerDisable.label = Betriebs-LED aus
channel-type.shelly.ledPowerDisable.description = ON: Die Betriebsanzeige (LED) am Gerät ist deaktiviert
channel-type.shelly.colorMode.label = Farbmodus
channel-type.shelly.colorMode.description = Betriebsart: ON: Farbe, OFF: Weiß
channel-type.shelly.colorFull.label = Voll-Farbe
channel-type.shelly.colorFull.description = Ausgewählte Farbe (red/green/blue/yellow/white) wird auf volle Intensität gesetzt (255)
channel-type.shelly.colorFull.state.option.red = Rot
channel-type.shelly.colorFull.state.option.green = Grün
channel-type.shelly.colorFull.state.option.blue = Blau
channel-type.shelly.colorFull.state.option.yellow = Gelb
channel-type.shelly.colorFull.state.option.white = Weiß
channel-type.shelly.colorRed.label = Rot
channel-type.shelly.colorRed.description = Rot-Anteil des RGB-Wertes (0-255)
channel-type.shelly.colorGreen.label = Grün
channel-type.shelly.colorGreen.description = Grün-Anteil des RGB-Wertes (0-255)
channel-type.shelly.colorBlue.label = Blau
channel-type.shelly.colorBlue.description = Blau-Anteil des RGB-Wertes (0-255)
channel-type.shelly.colorWhite.label = Weiß
channel-type.shelly.colorWhite.description = Weiß-Anteil des RGBW-Wertes (0-255)
channel-type.shelly.colorGain.label = Verstärkung
channel-type.shelly.colorGain.description = Verstärkung des Farbwertes (1-100%)
channel-type.shelly.whiteTemp.label = Lichttemperatur
channel-type.shelly.whiteTemp.description = Temperatur des Weißlichtes (Bulb/RGBW2: 3000..6500K; Duo: 2700K..6500K)
channel-type.shelly.colorEffectBulb.label = Lichteffekt
channel-type.shelly.colorEffectBulb.description = Lichteffekt: 0: keiner, 1: Meteoritenregen, 2: Verlauf, 3: Atmen, 4: Blitzen, 5: ‹bergang ein/aus, 6: Rot/Grün-Wechsel
channel-type.shelly.colorEffectBulb.option.0 = Aus
channel-type.shelly.colorEffectBulb.option.1 = Meteoritenregen
channel-type.shelly.colorEffectBulb.option.2 = Verlauf
channel-type.shelly.colorEffectBulb.option.3 = Atmen
channel-type.shelly.colorEffectBulb.option.4 = Blitzen
channel-type.shelly.colorEffectBulb.option.5 = Übergang ein/aus
channel-type.shelly.colorEffectBulb.option.6 = Rot/Grün-Wechsel
channel-type.shelly.colorEffectRGBW2.label = Farbeffekt
channel-type.shelly.colorEffectRGBW2.description = Lichteffekt: 0: keiner, 1: Meteoritenregen, 2: Farbverlauf, 3: Blitzen
channel-type.shelly.colorEffectRGBW2.option.0 = Aus
channel-type.shelly.colorEffectRGBW2.option.1 = Meteoritenregen
channel-type.shelly.colorEffectRGBW2.option.2 = Farbverlauf
channel-type.shelly.colorEffectRGBW2.option.3 = Blitzen
channel-type.shelly.sensorTemp.label = Temperatur
channel-type.shelly.sensorTemp.description = Aktuelle Temperatur des Sensors in ∞C
channel-type.shelly.sensorExtTemp.label = Temperatur
channel-type.shelly.sensorExtTemp.description = Aktuelle Temperatur des externen Sensors in ∞C
channel-type.shelly.sensorExtHum.label = Luftfeuchtigkeit
channel-type.shelly.sensorExtHum.description = Relative Luftfeuchtigkeit in %
channel-type.shelly.sensorHumidity.label = Luftfeuchtigkeit
channel-type.shelly.sensorHumidity.description = Relative Luftfeuchtigkeit in %
channel-type.shelly.sensorFlood.label = Wasseralarm
channel-type.shelly.sensorFlood.description = Alarm: Es wurde Wasser erkannt
channel-type.shelly.sensorSmoke.label = Rauchalarm
channel-type.shelly.sensorSmoke.description = Alarm: Es wurde Rauch erkannt
channel-type.shelly.sensorLux.label = Helligkeit
channel-type.shelly.sensorLux.description = Helligkeit in Lux
channel-type.shelly.sensorIllumination.label = Tageslicht
channel-type.shelly.sensorIllumination.description = Erkanntes Tageslicht (bright=taghell, twilight=Dämmerung, dark=Abend/Nacht)
channel-type.shelly.sensorIllumination.state.option.dark = Dunkel
channel-type.shelly.sensorIllumination.state.option.twilight = Dämmerung
channel-type.shelly.sensorIllumination.state.option.bright = Hell
channel-type.shelly.sensorIllumination.state.option.unknown = Unbekannt
channel-type.shelly.sensorIllumination.description = Angabe zum erkannten Tageslichtwert
channel-type.shelly.sensorPPM.label = Gas-Konzentration
channel-type.shelly.sensorPPM.description = Gemessene Konzentration in PPM
channel-type.shelly.sensorTilt.label = Öffnungswinkel
channel-type.shelly.sensorTilt.description = Öffnungswinkel in Grad (erfordert Kalibrierung in der App)
channel-type.shelly.sensorVibration.label = Vibration
channel-type.shelly.sensorVibration.description = ON: Sensor hat eine Vibration erkannt
channel-type.shelly.sensorValve.label = Ventil
channel-type.shelly.sensorValve.description = Gibt den Status des Ventils an, sofern eines angeschlossen ist.
channel-type.shelly.sensorValve.state.option.closed = geschlossen
channel-type.shelly.sensorValve.state.option.opened = geöffnet
channel-type.shelly.sensorValve.state.option.not_connected = nicht angeschlossen
channel-type.shelly.sensorValve.state.option.failure = Störung
channel-type.shelly.sensorValve.state.option.closing = schließt
channel-type.shelly.sensorValve.state.option.opening = öffnet
channel-type.shelly.sensorValve.state.option.checking = Test läuft
channel-type.shelly.sensorValve.state.option.UNKNOWN = unbekannt
channel-type.shelly.sensorCharger.label = Ladegerät
channel-type.shelly.sensorCharger.description = ON: Ein Ladegerät ist angeschlossen
channel-type.shelly.sensorContact.label = Kontakt
channel-type.shelly.sensorContact.description = Status des Sensors (OPEN=offen, CLOSED=geschlossen)
channel-type.shelly.sensorContact.state.option.OPEN = geöffnet
channel-type.shelly.sensorContact.state.option.CLOSED = geschlossen
channel-type.shelly.sensorState.label = Gerätestatus
channel-type.shelly.sensorState.description = Status der Betriebsbereitschaft (warmup/normal/fault)
channel-type.shelly.sensorState.state.option.warmup= Initialisierung
channel-type.shelly.sensorState.state.option.normal= Normal
channel-type.shelly.sensorState.state.option.fault= Fehler
channel-type.shelly.sensorState.state.option.unknown= Unbekannt
channel-type.shelly.sensorWakeup.label = Aufwachgrund
channel-type.shelly.sensorWakeup.description = Grund des letzten Aufweckens (button/battery/perodice/powerdown/sensor/alarm/ext_power)
channel-type.shelly.sensorWakeup.state.option.button = Taste
channel-type.shelly.sensorWakeup.state.option.battery = Batterie
channel-type.shelly.sensorWakeup.state.option.periodic = Intervall
channel-type.shelly.sensorWakeup.state.option.poweron = Betrieb
channel-type.shelly.sensorWakeup.state.option.sensor = Statusänderung
channel-type.shelly.sensorWakeup.state.option.alarm = Alarm
channel-type.shelly.sensorWakeup.state.option.ext_power = Ladegerät verbunden
channel-type.shelly.batVoltage.label = Batteriespannung
channel-type.shelly.batVoltage.description = Batteriespannung in Volt (V)
channel-type.shelly.charger.label = Ladegerät
channel-type.shelly.charger.description = ON: Ein Ladegerät ist angeschlossen
channel-type.shelly.senseKey.label = IR-Code
channel-type.shelly.senseKey.description = IR Code senden im Pronto- oder HEX64-Format
channel-type.shelly.deviceName.label = Gerätename
channel-type.shelly.deviceName.description = Symbolischer Name des Gerätes (Konfiguration über Shelly App)
channel-type.shelly.uptime.label = Laufzeit
channel-type.shelly.uptime.description = Anzahl Sekunden seit dem das Gerät mit Strom versorgt wurde
channel-type.shelly.heartBeat.label = Letzte Aktivität
channel-type.shelly.heartBeat.description = Zeitpunkt der letzten Aktivität. Hierbei kann es sich um einen erfolgreichen API-Aufruf, oder Sensor-Aktualisierung handeln. Dies schließt eine erfolgreiche Netzwerk-Kommunikation ein (WiFi + IP).
channel-type.shelly.updateAvailable.label = Firmwareaktualisierung vorhanden
channel-type.shelly.updateAvailable.description = ON: Es ist eine neuere Firmwareversion verfügbar (kann mit der Shelly App durchgef¸hrt werden)
channel-type.shelly.deviceTemp.label = Gerätetemperatur
channel-type.shelly.deviceTemp.description = Temperatur im Gerät. Hohe Temperaturen deuten ggf. auch ein Hitzestau/Installationsproblem hin.
channel-type.shelly.lastUpdate.label = Letzte Aktualisierung
channel-type.shelly.lastUpdate.description = Zeitstempel der letzten Aktualisierung (lokale Zeitzone)
channel-type.shelly.lastEvent.label = Letztes Ereignis
channel-type.shelly.lastEvent.description = Typ des letzten Ereignisses (S=kurz, SS=2xkurz, SSS=3xkurz, L=lang)
channel-type.shelly.lastEvent.state.option.S = 1x kurz
channel-type.shelly.lastEvent.state.option.SS = 2x kurz
channel-type.shelly.lastEvent.state.option.SSS = 3x kurz
channel-type.shelly.lastEvent.state.option.L = lang
channel-type.shelly.lastEvent.state.option.SL = kurz-lang
channel-type.shelly.lastEvent.state.option.LS = lang-kurz
channel-type.shelly.eventCount.label = Ereigniszähler
channel-type.shelly.eventCount.description = Anzahl der empfangenen Ereignisse.
channel-type.shelly.eventTrigger.label = Ereignis
channel-type.shelly.eventTrigger.description = Signalisiert Ereignisse (Trigger): ROLLER_OPEN=Rollladen geöffnet, ROLLER_CLOSE=Rollladen geschlossen, ROLLER_STOP=Rollladen angehalten
channel-type.shelly.eventTrigger.option.ROLLER_OPEN = Rollladen geöffnet
channel-type.shelly.eventTrigger.option.ROLLER_CLOSE = Rollladen geschlossen
channel-type.shelly.eventTrigger.option.ROLLER_STOP = Rollladen gestoppt
channel-type.shelly.alarmTrigger.description = Signalisiert Alarme (Trigger): NONE=kein Alarm, WEAK_SIGNAL=Schlechte WiFi-Verbindung, RESTARTED=Gerät neu gestartet, OVERTEMP=Überhitzung, OVERLOAD=Überlast, OVERPOWER=Maximale Last überschritten, LOAD_ERROR=Lastfehler, LOW_BATTERY=Batterie schwach
channel-type.shelly.alarmTrigger.option.NONE = kein Alarm
channel-type.shelly.alarmTrigger.option.WEAK_SIGNAL = Schlechte WiFi-Verbindung
channel-type.shelly.alarmTrigger.option.RESTARTED = Gerät neu gestartet
channel-type.shelly.alarmTrigger.option.OVERTEMP = Überhitzung
channel-type.shelly.alarmTrigger.option.OVERLOAD = Überlast
channel-type.shelly.alarmTrigger.option.OVERPOWER = Maximale Last überschritten
channel-type.shelly.alarmTrigger.option.LOAD_ERROR = Lastfehler
channel-type.shelly.alarmTrigger.option.LOW_BATTERY = Batterieladung schwach
channel-type.shelly.alarmState.label = Alarmstatus
channel-type.shelly.alarmState.description = Typ des Alarms (unknown/none/mild/heavy/test)
channel-type.shelly.alarmState.state.option.unknown = Unbekannt
channel-type.shelly.alarmState.state.option.none = Kein Alarm
channel-type.shelly.alarmState.state.option.mild = Leichte Konzentration
channel-type.shelly.alarmState.state.option.heavy = Hohe Konzentration
channel-type.shelly.alarmState.state.option.test = Testknopf gedrückt
channel-type.shelly.selfTest.label = Selbsttest
channel-type.shelly.selfTest.description = Status des Selbsttests
channel-type.shelly.selfTest.state.option.pending = ausstehend
channel-type.shelly.selfTest.state.option.not_completed = Nicht abgeschlossen
channel-type.shelly.selfTest.state.option.running = Test läuft
channel-type.shelly.selfTest.state.option.completed = abgeschlossen
channel-type.shelly.selfTest.state.option.unknown = unbekannt

View File

@@ -0,0 +1,122 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="shelly"
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-group-type id="deviceStatus">
<label>Device Status</label>
<description>Information about the device status</description>
<channels>
<channel id="alarm" typeId="alarmTrigger"/>
<channel id="wifiSignal" typeId="system.signal-strength"/>
<channel id="uptime" typeId="uptime"/>
</channels>
</channel-group-type>
<channel-type id="alarmTrigger">
<kind>trigger</kind>
<label>Alarm</label>
<description>Alarm Trigger, e.g. weak WiFi Signal detected or over heating.</description>
<event>
<options>
<option value="NONE">None</option>
<option value="WEAK_SIGNAL">Weak WiFi</option>
<option value="RESTARTED">Device restarted</option>
<option value="OVERTEMP">Device is overheating</option>
<option value="OVERLOAD">Device is overloaded</option>
<option value="OVERPOWER">Device is over max power</option>
<option value="LOAD_ERROR">Load error</option>
<option value="LOW_BATTERY">Low battery</option>
<option value="BATTERY">Wakeup by battery</option>
<option value="POWERON">Device was powered on</option>
<option value="BUTTON">Button was pressed</option>
<option value="SENSOR">Sensor data updated</option>
</options>
</event>
</channel-type>
<channel-type id="sensorWakeup" advanced="true">
<item-type>String</item-type>
<label>Wakeup Reason</label>
<description>Last reason, which woke up the device</description>
<state pattern="%s" readOnly="true">
<options>
<option value="button">Button</option>
<option value="battery">Battery</option>
<option value="periodic">Periodic</option>
<option value="poweron">Power-On</option>
<option value="sensor">Sensor</option>
<option value="alarm">Alarm</option>
<option value="ext_power">Charger connected</option>
</options>
</state>
</channel-type>
<channel-type id="deviceName">
<item-type>String</item-type>
<label>@text/channel-type.shelly.deviceName.label</label>
<description>@text/channel-type.shelly.deviceName.description</description>
<state readOnly="true">
</state>
</channel-type>
<channel-type id="charger" advanced="true">
<item-type>Switch</item-type>
<label>Charger Connected</label>
<description>ON: Device is charging, OFF: No charger connected</description>
<state readOnly="true">
</state>
</channel-type>
<channel-type id="batVoltage" advanced="true">
<item-type>Number:ElectricPotential</item-type>
<label>Battery Voltage</label>
<description>Battery voltage in V</description>
<state readOnly="true" pattern="%f %unit%">
</state>
</channel-type>
<channel-type id="uptime" advanced="true">
<item-type>Number:Time</item-type>
<label>Uptime</label>
<description>Number of seconds since the device was powered up</description>
<state readOnly="true" pattern="%d %unit%">
</state>
</channel-type>
<channel-type id="heartBeat" advanced="true">
<item-type>DateTime</item-type>
<label>Last Heartbeat</label>
<description>Last time we received a signal from the device (API result or sensor report), This indicates that device
communication works on the network layer (WiFi + IP).</description>
<state readOnly="true">
</state>
</channel-type>
<channel-type id="updateAvailable" advanced="true">
<item-type>Switch</item-type>
<label>Update available</label>
<description>ON: A firmware update is available (use Shelly App to perform update)</description>
<state readOnly="true">
</state>
</channel-type>
<channel-type id="deviceTemp" advanced="true">
<item-type>Number:Temperature</item-type>
<label>Internal Temperature</label>
<description>Internal device temperature, helps to detect overheating due to installation issues</description>
<category>Temperature</category>
<tags>
<tag>CurrentTemperature</tag>
</tags>
<state readOnly="true" pattern="%f %unit%">
</state>
</channel-type>
<channel-type id="selfTest">
<item-type>String</item-type>
<label>Self Test</label>
<description>Self Test Status/Result</description>
<state pattern="%s" readOnly="true">
<options>
<option value="not_completed">Not completed</option>
<option value="completed">Completed</option>
<option value="running">Running</option>
<option value="pending">Pending</option>
</options>
</state>
</channel-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,264 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="shelly"
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="shellybulb">
<label>Shelly Bulb (SHBULB)</label>
<description>Shelly Bulb</description>
<channel-groups>
<channel-group id="control" typeId="bulbControl"/>
<channel-group id="color" typeId="colorSettingsBulb"/>
<channel-group id="white" typeId="whiteSettings"/>
<channel-group id="device" typeId="deviceStatus"/>
</channel-groups>
<config-description-ref uri="thing-type:shelly:light"/>
</thing-type>
<thing-type id="shellybulbduo">
<label>Shelly Duo (SHBDUO-1)</label>
<description>Shelly Duo</description>
<channel-groups>
<channel-group id="control" typeId="duoControl"/>
<channel-group id="white" typeId="whiteSettings"/>
<channel-group id="meter" typeId="meter"/>
<channel-group id="device" typeId="deviceStatus"/>
</channel-groups>
<config-description-ref uri="thing-type:shelly:light"/>
</thing-type>
<thing-type id="shellyvintage">
<label>Shelly Vintage (SHVIN-1)</label>
<description>Shelly Vintage Light Bulb</description>
<channel-groups>
<channel-group id="control" typeId="duoControl"/>
<channel-group id="white" typeId="whiteSettingsSimple"/>
<channel-group id="meter" typeId="meter"/>
<channel-group id="device" typeId="deviceStatus"/>
</channel-groups>
<config-description-ref uri="thing-type:shelly:light"/>
</thing-type>
<thing-type id="shellyrgbw2-color">
<label>Shelly RGBW2 Color Mode (SHRGBW2)</label>
<description>Shelly RGBW2 Controller - Color Mode</description>
<category>Lightbulb</category>
<channel-groups>
<channel-group id="control" typeId="rgbw2ColorControl"/>
<channel-group id="color" typeId="colorSettingsRGBW2"/>
<channel-group id="meter" typeId="meter"/>
<channel-group id="device" typeId="deviceStatus"/>
</channel-groups>
<config-description-ref uri="thing-type:shelly:rgbw2"/>
</thing-type>
<thing-type id="shellyrgbw2-white">
<label>Shelly RGBW2 White Mode (SHRGBW2)</label>
<description>Shelly RGBW2 Controller - White Mode</description>
<channel-groups>
<channel-group id="control" typeId="rgbw2WhiteControl"/>
<channel-group id="channel1" typeId="rgbw2Channel">
<label>Channel 1</label>
</channel-group>
<channel-group id="channel2" typeId="rgbw2Channel">
<label>Channel 2</label>
</channel-group>
<channel-group id="channel3" typeId="rgbw2Channel">
<label>Channel 3</label>
</channel-group>
<channel-group id="channel4" typeId="rgbw2Channel">
<label>Channel 4</label>
</channel-group>
<channel-group id="meter" typeId="meter"/>
<channel-group id="device" typeId="deviceStatus"/>
</channel-groups>
<config-description-ref uri="thing-type:shelly:rgbw2"/>
</thing-type>
<channel-group-type id="bulbControl">
<label>Light Control</label>
<description>Control your light</description>
<channels>
<channel id="power" typeId="system.power"/>
<channel id="mode" typeId="colorMode"/>
<channel id="autoOn" typeId="timerAutoOn"/>
<channel id="autoOff" typeId="timerAutoOff"/>
<channel id="timerActive" typeId="timerActive"/>
</channels>
</channel-group-type>
<channel-group-type id="duoControl">
<label>Light Control</label>
<description>Control your light</description>
<channels>
<channel id="autoOn" typeId="timerAutoOn"/>
<channel id="autoOff" typeId="timerAutoOff"/>
<channel id="timerActive" typeId="timerActive"/>
</channels>
</channel-group-type>
<channel-group-type id="rgbw2ColorControl">
<label>Light Control</label>
<description>Control your light channels</description>
<channels>
<channel id="power" typeId="system.power"/>
<channel id="autoOn" typeId="timerAutoOn"/>
<channel id="autoOff" typeId="timerAutoOff"/>
<channel id="timerActive" typeId="timerActive"/>
</channels>
</channel-group-type>
<channel-group-type id="rgbw2WhiteControl">
<label>White Control</label>
</channel-group-type>
<channel-group-type id="colorSettingsBulb">
<label>Colors</label>
<description>Light colors</description>
<channels>
<channel id="hsb" typeId="system.color"/>
<channel id="full" typeId="colorFull"/>
<channel id="red" typeId="colorRed"/>
<channel id="green" typeId="colorGreen"/>
<channel id="blue" typeId="colorBlue"/>
<channel id="white" typeId="colorWhite"/>
<channel id="gain" typeId="colorGain"/>
<channel id="effect" typeId="colorEffectBulb"/>
</channels>
</channel-group-type>
<channel-group-type id="colorSettingsRGBW2">
<label>Colors</label>
<description>Light colors</description>
<channels>
<channel id="hsb" typeId="system.color"/>
<channel id="full" typeId="colorFull"/>
<channel id="red" typeId="colorRed"/>
<channel id="green" typeId="colorGreen"/>
<channel id="blue" typeId="colorBlue"/>
<channel id="white" typeId="colorWhite"/>
<channel id="gain" typeId="colorGain"/>
<channel id="effect" typeId="colorEffectRGBW2"/>
</channels>
</channel-group-type>
<channel-group-type id="whiteSettings">
<label>White settings</label>
<description>Adjust colors when device is in white mode</description>
<channels>
<channel id="brightness" typeId="whiteBrightness"/>
<channel id="temperature" typeId="whiteTemp"/>
</channels>
</channel-group-type>
<channel-group-type id="whiteSettingsSimple">
<label>White settings</label>
<description>Adjust colors when device is in white mode</description>
<channels>
<channel id="brightness" typeId="whiteBrightness"/>
</channels>
</channel-group-type>
<channel-group-type id="rgbw2Channel">
<label>Control</label>
<description>Switch channel and adjust settings</description>
<channels>
<channel id="autoOn" typeId="timerAutoOn"/>
<channel id="autoOff" typeId="timerAutoOff"/>
<channel id="timerActive" typeId="timerActive"/>
<channel id="brightness" typeId="whiteBrightness"/>
</channels>
</channel-group-type>
<channel-type id="colorMode">
<item-type>Switch</item-type>
<label>Mode (ON=color, OFF=white)</label>
<description>ON: Device is in color mode, OFF: Device is in White Mode</description>
</channel-type>
<channel-type id="colorFull">
<item-type>String</item-type>
<label>Full Color</label>
<description>Setting this channel sets the selected color to 255, all others to 0</description>
<state>
<options>
<option value="red">Red</option>
<option value="green">Green</option>
<option value="blue">Blue</option>
<option value="yellow">Yellow</option>
<option value="white">White</option>
</options>
</state>
</channel-type>
<channel-type id="colorRed" advanced="true">
<item-type>Dimmer</item-type>
<label>Red</label>
<description>red, 0..255, only in Color Mode</description>
<state min="0" max="255" step="1" readOnly="false"></state>
</channel-type>
<channel-type id="colorGreen" advanced="true">
<item-type>Dimmer</item-type>
<label>Green</label>
<description>green, 0..255, applies Color Mode</description>
<state min="0" max="255" step="1" readOnly="false"></state>
</channel-type>
<channel-type id="colorBlue" advanced="true">
<item-type>Dimmer</item-type>
<label>Blue</label>
<description>blue, 0..255, only in Color Mode</description>
<state min="0" max="255" step="1" readOnly="false"></state>
</channel-type>
<channel-type id="colorWhite" advanced="true">
<item-type>Dimmer</item-type>
<label>White</label>
<description>white, 0..255, applies in Color Mode</description>
<state min="0" max="255" step="1" readOnly="false"></state>
</channel-type>
<channel-type id="colorGain">
<item-type>Dimmer</item-type>
<label>Gain</label>
<description>gain for all channels, 0..100, applies in Color Mode</description>
<state min="0" max="100" step="1" readOnly="false"></state>
</channel-type>
<channel-type id="whiteTemp">
<item-type>Dimmer</item-type>
<label>Light Temperature</label>
<description>Light Temperature 3000..6500K</description>
<state min="3000" max="6500" step="10" readOnly="false"></state>
</channel-type>
<channel-type id="whiteBrightness">
<item-type>Dimmer</item-type>
<label>Brightness</label>
<description>Brightness: 0..100%</description>
<category>DimmableLight</category>
</channel-type>
<channel-type id="colorEffectBulb">
<item-type>Number</item-type>
<label>Effect</label>
<description>Currently applied effect, description: 0: Off, 1: Meteor Shower, 2: Gradual Change, 3: Breath, 4: Flash,
5: On/Off Gradual, 6: Red/Green Change</description>
<state>
<options>
<option value="0">Off</option>
<option value="1">Meteor Shower</option>
<option value="2">Gradual Change</option>
<option value="3">Breath</option>
<option value="4">Flash</option>
<option value="5">On/Off Gradual</option>
<option value="6">Red/Green Change</option>
</options>
</state>
</channel-type>
<channel-type id="colorEffectRGBW2">
<item-type>Number</item-type>
<label>Effect</label>
<description>Currently applied effect, description: 0: Off, 1: Meteor Shower, 2: Gradual Change, 3: Flash</description>
<state>
<options>
<option value="0">Off</option>
<option value="1">Meteor Shower</option>
<option value="2">Gradual Change</option>
<option value="3">Flash</option>
</options>
</state>
</channel-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,528 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="shelly"
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="shelly1">
<label>Shelly1 (SHSW-1)</label>
<description>Shelly1 device with single relay</description>
<channel-groups>
<channel-group id="relay" typeId="relayChannel"/>
<channel-group id="sensors" typeId="externalSensors"/>
<channel-group id="device" typeId="deviceStatus"/>
</channel-groups>
<config-description-ref uri="thing-type:shelly:relay"/>
</thing-type>
<thing-type id="shelly1pm">
<label>Shelly1PM (SHSW-PM)</label>
<description>Shelly1PM device with single relay and power meter</description>
<channel-groups>
<channel-group id="relay" typeId="relayChannel"/>
<channel-group id="meter" typeId="meter"/>
<channel-group id="sensors" typeId="externalSensors"/>
<channel-group id="device" typeId="deviceStatus"/>
</channel-groups>
<config-description-ref uri="thing-type:shelly:relay"/>
</thing-type>
<thing-type id="shellyem">
<label>ShellyEM (SHEM)</label>
<description>Shelly EM device with single relay and power meter</description>
<channel-groups>
<channel-group id="meter1" typeId="meter">
<label>Power Meter 1</label>
</channel-group>
<channel-group id="meter2" typeId="meter">
<label>Power Meter 2</label>
</channel-group>
<channel-group id="relay" typeId="relayChannel"/>
<channel-group id="device" typeId="deviceStatus"/>
</channel-groups>
<config-description-ref uri="thing-type:shelly:relay"/>
</thing-type>
<!-- Shelly 3EM - device reports wrong service name: shellyem3, see README -->
<thing-type id="shellyem3">
<label>Shelly 3EM (SHEM-3)</label>
<description>Shelly 3EM device with 3 power meters and a relay (thing name is shellyem3, see README)</description>
<channel-groups>
<channel-group id="meter1" typeId="meter">
<label>Power Meter 1</label>
</channel-group>
<channel-group id="meter2" typeId="meter">
<label>Power Meter 2</label>
</channel-group>
<channel-group id="meter3" typeId="meter">
<label>Power Meter 3</label>
</channel-group>
<channel-group id="relay" typeId="relayChannel"/>
<channel-group id="device" typeId="deviceStatus"/>
</channel-groups>
<config-description-ref uri="thing-type:shelly:relay"/>
</thing-type>
<thing-type id="shelly2-relay">
<label>Shelly2 Relay (SHSW-21)</label>
<description>Shelly2 device with two relays</description>
<channel-groups>
<channel-group id="relay1" typeId="relayChannel">
<label>Relay 1</label>
</channel-group>
<channel-group id="relay2" typeId="relayChannel">
<label>Relay 2</label>
</channel-group>
<channel-group id="meter" typeId="meter"/>
<channel-group id="device" typeId="deviceStatus"/>
</channel-groups>
<config-description-ref uri="thing-type:shelly:relay"/>
</thing-type>
<thing-type id="shelly2-roller">
<label>Shelly2 Roller (SHSW-21)</label>
<description>Shelly2 in Roller Mode</description>
<channel-groups>
<channel-group id="roller" typeId="rollerControl"/>
<channel-group id="meter" typeId="meter"/>
<channel-group id="device" typeId="deviceStatus"/>
</channel-groups>
<config-description-ref uri="thing-type:shelly:roller"/>
</thing-type>
<thing-type id="shelly25-relay">
<label>Shelly2.5 Relay (SHSW-25)</label>
<description>Shelly2.5 device with two relays, 2 meters</description>
<channel-groups>
<channel-group id="relay1" typeId="relayChannel">
<label>Relay 1</label>
</channel-group>
<channel-group id="meter1" typeId="meter">
<label>Power Meter 1</label>
</channel-group>
<channel-group id="relay2" typeId="relayChannel">
<label>Relay 2</label>
</channel-group>
<channel-group id="meter2" typeId="meter">
<label>Power Meter 2</label>
</channel-group>
<channel-group id="device" typeId="deviceStatus"/>
</channel-groups>
<config-description-ref uri="thing-type:shelly:relay"/>
</thing-type>
<thing-type id="shelly25-roller">
<label>Shelly2.5 Roller (SHSW-25)</label>
<description>Shelly2 in Roller Mode</description>
<channel-groups>
<channel-group id="roller" typeId="rollerControl"/>
<channel-group id="meter" typeId="meter"/>
<channel-group id="device" typeId="deviceStatus"/>
</channel-groups>
<config-description-ref uri="thing-type:shelly:roller"/>
</thing-type>
<thing-type id="shelly4pro">
<label>Shelly4 Pro Relay (SHSW-44)</label>
<description>Shelly4Pro device with 4 relays</description>
<channel-groups>
<channel-group id="relay1" typeId="relayChannel">
<label>Relay 1</label>
</channel-group>
<channel-group id="meter1" typeId="meter">
<label>Power Meter 1</label>
</channel-group>
<channel-group id="relay2" typeId="relayChannel">
<label>Relay 2</label>
</channel-group>
<channel-group id="meter2" typeId="meter">
<label>Power Meter 2</label>
</channel-group>
<channel-group id="relay3" typeId="relayChannel">
<label>Relay 3</label>
</channel-group>
<channel-group id="meter3" typeId="meter">
<label>Power Meter 3</label>
</channel-group>
<channel-group id="relay4" typeId="relayChannel">
<label>Relay 4</label>
</channel-group>
<channel-group id="meter4" typeId="meter">
<label>Power Meter 4</label>
</channel-group>
<channel-group id="device" typeId="deviceStatus"/>
</channel-groups>
<config-description-ref uri="thing-type:shelly:relay"/>
</thing-type>
<thing-type id="shellyplug">
<label>Shelly Plug</label>
<description>Shelly Plug device with relay and power meter</description>
<channel-groups>
<channel-group id="relay" typeId="relayChannelPlug"/>
<channel-group id="meter" typeId="meter"/>
<channel-group id="device" typeId="deviceStatus"/>
</channel-groups>
<config-description-ref uri="thing-type:shelly:relay"/>
</thing-type>
<thing-type id="shellyplugs">
<label>Shelly Plug-S (SHPLG-S)</label>
<description>Shelly Plug-S with relay, meter and LED control</description>
<channel-groups>
<channel-group id="relay" typeId="relayChannelPlug"/>
<channel-group id="meter" typeId="meter"/>
<channel-group id="device" typeId="deviceStatus"/>
</channel-groups>
<config-description-ref uri="thing-type:shelly:relay"/>
</thing-type>
<thing-type id="shellydimmer">
<label>Shelly Dimmer (SHDM-1)</label>
<description>Shelly Dimmer</description>
<category>DimmableLight</category>
<channel-groups>
<channel-group id="relay" typeId="dimmerChannel"/>
<channel-group id="meter" typeId="meter"/>
<channel-group id="device" typeId="deviceStatus"/>
</channel-groups>
<config-description-ref uri="thing-type:shelly:dimmer"/>
</thing-type>
<thing-type id="shellydimmer2">
<label>Shelly Dimmer 2 (SHDM-2)</label>
<description>Shelly Dimmer 2</description>
<category>DimmableLight</category>
<channel-groups>
<channel-group id="relay" typeId="dimmerChannel"/>
<channel-group id="meter" typeId="meter"/>
<channel-group id="device" typeId="deviceStatus"/>
</channel-groups>
<config-description-ref uri="thing-type:shelly:dimmer"/>
</thing-type>
<thing-type id="shellyix3">
<label>Shelly ix3 (SHIX3-1)</label>
<description>Shelly i3 Device with 3 inputs</description>
<channel-groups>
<channel-group id="status1" typeId="ix3Channel"/>
<channel-group id="status2" typeId="ix3Channel"/>
<channel-group id="status3" typeId="ix3Channel"/>
<channel-group id="device" typeId="deviceStatus"/>
</channel-groups>
<config-description-ref uri="thing-type:shelly:relay"/>
</thing-type>
<channel-group-type id="relayChannel">
<label>Relay</label>
<description>A Shelly relay channel</description>
<channels>
<channel id="output" typeId="system.power"/>
<channel id="input" typeId="inputState"/>
<channel id="button" typeId="system.button"/>
<channel id="autoOn" typeId="timerAutoOn"/>
<channel id="autoOff" typeId="timerAutoOff"/>
<channel id="timerActive" typeId="timerActive"/>
</channels>
</channel-group-type>
<channel-group-type id="relayChannelPlug">
<label>Relay</label>
<description>A Shelly relay channel</description>
<channels>
<channel id="output" typeId="system.power"/>
<channel id="autoOn" typeId="timerAutoOn"/>
<channel id="autoOff" typeId="timerAutoOff"/>
<channel id="timerActive" typeId="timerActive"/>
</channels>
</channel-group-type>
<channel-group-type id="dimmerChannel">
<label>Dimmer</label>
<description>A Shelly Dimmer channel</description>
<channels>
<channel id="brightness" typeId="dimmerBrightness"/>
<channel id="input1" typeId="inputState1"/>
<channel id="input2" typeId="inputState2"/>
<channel id="button" typeId="system.button"/>
<channel id="autoOn" typeId="timerAutoOn"/>
<channel id="autoOff" typeId="timerAutoOff"/>
</channels>
</channel-group-type>
<channel-group-type id="ix3Channel">
<label>Input</label>
<description>Input Status</description>
<channels>
<channel id="input" typeId="inputState"/>
<channel id="button" typeId="system.button"/>
<channel id="lastEvent" typeId="lastEvent"/>
<channel id="eventCount" typeId="eventCount"/>
</channels>
</channel-group-type>
<channel-group-type id="rollerControl">
<label>Roller Control</label>
<description>Controlling the roller mode</description>
<channels>
<channel id="control" typeId="rollerShutter"/>
<channel id="rollerpos" typeId="rollerPosition"/>
<channel id="state" typeId="rollerState"/>
<channel id="stopReason" typeId="rollerStop"/>
<channel id="input1" typeId="inputState1"/>
<channel id="input2" typeId="inputState2"/>
<channel id="event" typeId="eventTrigger"/>
</channels>
</channel-group-type>
<channel-group-type id="meter">
<label>Power Meter</label>
<description>Power consumption for the relay</description>
</channel-group-type>
<channel-group-type id="externalSensors">
<label>External Sensors</label>
<description>Temperatures from external sensors connected to the optional Addon</description>
</channel-group-type>
<channel-type id="outputName">
<item-type>String</item-type>
<label>@text/channel-type.shelly.outputName.label</label>
<description>@text/channel-type.shelly.outputName.description</description>
<state readOnly="true">
</state>
</channel-type>
<channel-type id="timerAutoOn" advanced="true">
<item-type>Number:Time</item-type>
<label>Auto-ON Timer</label>
<description>ON: After the output was turned off it turns on automatically after xx seconds; 0 disables the timer</description>
<state min="0" step="1" pattern="%d %unit%" readOnly="false"></state>
</channel-type>
<channel-type id="timerAutoOff" advanced="true">
<item-type>Number:Time</item-type>
<label>Auto-OFF Timer</label>
<description>ON: After the output was turned on it turns off automatically after xx seconds; 0 disables the timer</description>
<state min="0" step="1" pattern="%d %unit%" readOnly="false"></state>
</channel-type>
<channel-type id="timerActive" advanced="true">
<item-type>Switch</item-type>
<label>Auto ON/OFF timer active</label>
<description>ON: A timer is active, OFF: no timer active</description>
<state readOnly="true">
</state>
</channel-type>
<channel-type id="rollerShutter">
<item-type>Rollershutter</item-type>
<label>Roller Control (0=open, 100=closed))</label>
<description>Controls the roller</description>
<category>Blinds</category>
</channel-type>
<channel-type id="rollerPosition" advanced="true">
<item-type>Dimmer</item-type>
<label>Roller Position (100=open, 0=closed)</label>
<description>Position the roller (100..0 in %, where 100%=open, 0%=closed)</description>
<state readOnly="false" min="0" max="100"/>
</channel-type>
<channel-type id="rollerState">
<item-type>String</item-type>
<label>State</label>
<description>State of the roller (open/close/stop)</description>
<state readOnly="true">
<options>
<option value="open">opening</option>
<option value="close">closing</option>
<option value="stop">stopped</option>
</options>
</state>
</channel-type>
<channel-type id="rollerStop">
<item-type>String</item-type>
<label>Roller stop reason</label>
<description>Last cause for stopping: normal, safety switch, obstacle</description>
<state readOnly="true">
<options>
<option value="normal">normal</option>
<option value="safety_switch">safety switch</option>
<option value="obstacle">obstacle detected</option>
</options>
</state>
</channel-type>
<channel-type id="rollerDirection">
<item-type>String</item-type>
<label>Last Roller Direction</label>
<description>Last direction: open or close</description>
<state readOnly="true">
<options>
<option value="open">open</option>
<option value="close">close</option>
<option value="stop">stopped</option>
</options>
</state>
</channel-type>
<channel-type id="inputState">
<item-type>Switch</item-type>
<label>Input</label>
<description>Input/Button state</description>
<state readOnly="true">
</state>
</channel-type>
<channel-type id="inputState1" advanced="true">
<item-type>Switch</item-type>
<label>Input #1</label>
<description>Input/Button state #1</description>
<state readOnly="true">
</state>
</channel-type>
<channel-type id="inputState2" advanced="true">
<item-type>Switch</item-type>
<label>Input #2</label>
<description>Input/Button state #2</description>
<state readOnly="true">
</state>
</channel-type>
<channel-type id="dimmerBrightness">
<item-type>Dimmer</item-type>
<label>Brightness</label>
<description>Light brightness in %</description>
<category>DimmableLight</category>
</channel-type>
<channel-type id="meterWatts">
<item-type>Number:Power</item-type>
<label>Watt</label>
<description>Current power consumption in Watt</description>
<state readOnly="true" pattern="%f %unit%">
</state>
</channel-type>
<channel-type id="meterAccuWatts" advanced="true">
<item-type>Number:Power</item-type>
<label>Accumulated Watt</label>
<description>Accumulated current power consumption in Watt from all meters</description>
<state readOnly="true" pattern="%f %unit%">
</state>
</channel-type>
<channel-type id="meterAccuTotal" advanced="true">
<item-type>Number:Power</item-type>
<label>Accumulated Total</label>
<description>Accumulated total power consumption in kw/h from all meters</description>
<state readOnly="true" pattern="%f %unit%">
</state>
</channel-type>
<channel-type id="meterAccuReturned" advanced="true">
<item-type>Number:Power</item-type>
<label>Accumulated Returned</label>
<description>Accumulated returned power consumption in kw/h from all meters</description>
<state readOnly="true" pattern="%f %unit%">
</state>
</channel-type>
<channel-type id="meterReactive">
<item-type>Number:Power</item-type>
<label>Reactive Watt</label>
<description>Instantaneous reactive power in Watts (W)</description>
<state readOnly="true" pattern="%f %unit%">
</state>
</channel-type>
<channel-type id="lastPower1" advanced="true">
<item-type>Number:Energy</item-type>
<label>Last Power #1</label>
<description>Last power consumption #1 - one rounded minute</description>
<state readOnly="true" pattern="%f %unit%">
</state>
</channel-type>
<channel-type id="lastPower2" advanced="true">
<item-type>Number:Energy</item-type>
<label>Last Power #2</label>
<description>Last power consumption #2 - one rounded minute</description>
<state readOnly="true" pattern="%f %unit%">
</state>
</channel-type>
<channel-type id="lastPower3" advanced="true">
<item-type>Number:Energy</item-type>
<label>Last Power #3</label>
<description>Last power consumption #3 - one rounded minute</description>
<state readOnly="true" pattern="%f %unit%">
</state>
</channel-type>
<channel-type id="meterTotal">
<item-type>Number:Energy</item-type>
<label>Total Energy</label>
<description>Total power consumption in kw/h</description>
<state readOnly="true" pattern="%f %unit%">
</state>
</channel-type>
<channel-type id="meterReturned">
<item-type>Number:Energy</item-type>
<label>Total Returned Energy (kw/h)</label>
<description>Total returned energy in kw/h</description>
<state readOnly="true" pattern="%f %unit%">
</state>
</channel-type>
<channel-type id="meterVoltage">
<item-type>Number:ElectricPotential</item-type>
<label>Voltage</label>
<description>RMS voltage, Volts </description>
<state readOnly="true" pattern="%f %unit%">
</state>
</channel-type>
<channel-type id="meterCurrent">
<item-type>Number:ElectricPotential</item-type>
<label>Current</label>
<description>Current in A </description>
<state readOnly="true" pattern="%f %unit%">
</state>
</channel-type>
<channel-type id="meterPowerFactor">
<item-type>Number</item-type>
<label>Power Factor</label>
<description></description>
<state readOnly="true" pattern="%f %unit%">
</state>
</channel-type>
<channel-type id="timestamp">
<item-type>DateTime</item-type>
<label>Last Update</label>
<description>Timestamp of last measurement</description>
<state readOnly="true">
</state>
</channel-type>
<channel-type id="ledStatusDisable">
<item-type>Switch</item-type>
<label>Disable Status LED</label>
<description>Disable LED indication for network status</description>
</channel-type>
<channel-type id="ledPowerDisable">
<item-type>Switch</item-type>
<label>Disable Power LED</label>
<description>Disable LED indication for output status</description>
</channel-type>
<channel-type id="lastUpdate" advanced="true">
<item-type>DateTime</item-type>
<label>Last Update</label>
<description>Timestamp of last status update</description>
<state readOnly="true">
</state>
</channel-type>
<channel-type id="eventTrigger">
<kind>trigger</kind>
<label>Event</label>
<description>Event Trigger, e.g. button on/off, Output on/off</description>
<event>
<options>
<option value="ROLLER_OPEN">Roller is open</option>
<option value="ROLLER_CLOSE">Roller is closed</option>
<option value="ROLLER_STOP">Roller has stopped</option>
</options>
</event>
</channel-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,347 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="shelly"
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="shellyht">
<label>Shelly H&amp;T (SHHT-1)</label>
<description>Shelly H&amp;T Sensor</description>
<channel-groups>
<channel-group id="sensors" typeId="htSensor"/>
<channel-group id="battery" typeId="batteryStatus"/>
<channel-group id="device" typeId="deviceStatus"/>
</channel-groups>
<config-description-ref uri="thing-type:shelly:battery"/>
</thing-type>
<thing-type id="shellysmoke">
<label>Shelly Smoke</label>
<description>Shelly Smoke Sensor (battery powered)</description>
<channel-groups>
<channel-group id="sensors" typeId="smokeSensor"/>
<channel-group id="battery" typeId="batteryStatus"/>
<channel-group id="device" typeId="deviceStatus"/>
</channel-groups>
<config-description-ref uri="thing-type:shelly:battery"/>
</thing-type>
<thing-type id="shellygas">
<label>Shelly GAS (SHGS-1)</label>
<description>Shelly Gas Sensor</description>
<channel-groups>
<channel-group id="sensors" typeId="gasSensor"/>
<channel-group id="device" typeId="deviceStatus"/>
</channel-groups>
<config-description-ref uri="thing-type:shelly:basic"/>
</thing-type>
<thing-type id="shellyflood">
<label>Shelly Flood (SHWT-1)</label>
<description>Shelly Flood Sensor (battery powered)</description>
<channel-groups>
<channel-group id="sensors" typeId="floodSensor"/>
<channel-group id="battery" typeId="batteryStatus"/>
<channel-group id="device" typeId="deviceStatus"/>
</channel-groups>
<config-description-ref uri="thing-type:shelly:battery"/>
</thing-type>
<thing-type id="shellydw">
<label>Shelly Door/Window (SHDW-1)</label>
<description>Shelly Door/Window Sensor (battery powered)</description>
<channel-groups>
<channel-group id="sensors" typeId="doorWinSensors"/>
<channel-group id="battery" typeId="batteryStatus"/>
<channel-group id="device" typeId="deviceStatus"/>
</channel-groups>
<config-description-ref uri="thing-type:shelly:battery"/>
</thing-type>
<thing-type id="shellydw2">
<label>Shelly Door/Window (SHDW-2)</label>
<description>Shelly Door/Window 2 Sensor (battery powered)</description>
<channel-groups>
<channel-group id="sensors" typeId="doorWinSensors"/>
<channel-group id="battery" typeId="batteryStatus"/>
<channel-group id="device" typeId="deviceStatus"/>
</channel-groups>
<config-description-ref uri="thing-type:shelly:battery"/>
</thing-type>
<thing-type id="shellysense">
<label>Shelly Sense (SHSEN-1)</label>
<description>Shelly Sense Remote IR Controller</description>
<channel-groups>
<channel-group id="control" typeId="senseControl"/>
<channel-group id="sensors" typeId="senseSensors"/>
<channel-group id="battery" typeId="batteryStatus"/>
<channel-group id="device" typeId="deviceStatus"/>
</channel-groups>
<config-description-ref uri="thing-type:shelly:battery"/>
</thing-type>
<thing-type id="shellybutton1">
<label>Shelly Button 1 (SHBTN-1)</label>
<description>Shelly Button 1 (battery powered)</description>
<channel-groups>
<channel-group id="status" typeId="buttonState"/>
<channel-group id="battery" typeId="batteryStatus"/>
<channel-group id="device" typeId="deviceStatus"/>
</channel-groups>
<config-description-ref uri="thing-type:shelly:battery"/>
</thing-type>
<channel-group-type id="htSensor">
<label>Sensor Data</label>
<description>Data from the HT Sensor</description>
</channel-group-type>
<channel-group-type id="floodSensor">
<label>Sensor Data</label>
<description>Data from the Flood Sensor</description>
</channel-group-type>
<channel-group-type id="smokeSensor">
<label>Sensor Data</label>
<description>Data from the Flood Sensor</description>
</channel-group-type>
<channel-group-type id="gasSensor">
<label>Sensor Data</label>
<description>Data from the Gas Sensor</description>
</channel-group-type>
<channel-group-type id="doorWinSensors">
<label>Sensors</label>
<description>Data from the sensors</description>
</channel-group-type>
<channel-group-type id="senseSensors">
<label>Sensors</label>
<description>Data from the Sense sensors</description>
</channel-group-type>
<channel-group-type id="batteryStatus">
<label>Battery Status</label>
</channel-group-type>
<channel-group-type id="senseControl">
<label>Sense Control</label>
<description>Sense Settings</description>
<channels>
<channel id="key" typeId="senseKey"/>
</channels>
</channel-group-type>
<channel-group-type id="buttonState">
<label>Button State</label>
<description>Status of the Button</description>
<channels>
<channel id="input" typeId="inputState"/>
<channel id="button" typeId="system.button"/>
<channel id="lastEvent" typeId="lastEvent"/>
<channel id="eventCount" typeId="eventCount"/>
</channels>
</channel-group-type>
<channel-type id="sensorContact">
<item-type>Contact</item-type>
<label>Contact</label>
<description>State of the contact: open/close</description>
<category>Contact</category>
<state pattern="%s" readOnly="true">
<options>
<option value="OPEN">Open</option>
<option value="CLOSED">Closed</option>
</options>
</state>
</channel-type>
<channel-type id="sensorState">
<item-type>String</item-type>
<label>Sensor State</label>
<description>Sensor State: normal</description>
<state pattern="%s" readOnly="true">
<options>
<option value="warmup">Warm-Up</option>
<option value="normal">Normal</option>
<option value="fault">Fault</option>
<option value="unknown">unknown</option>
</options>
</state>
</channel-type>
<channel-type id="alarmState">
<item-type>String</item-type>
<label>Alarm State</label>
<description>Alarm State: normal</description>
<state pattern="%s" readOnly="true">
<options>
<option value="unknown">Unknown</option>
<option value="none">None</option>
<option value="mild">Mild</option>
<option value="heavy">Heavy</option>
<option value="test">Test</option>
</options>
</state>
</channel-type>
<channel-type id="lastEvent">
<item-type>String</item-type>
<label>Event</label>
<description>Event Type</description>
<state pattern="%s" readOnly="true">
<options>
<option value="S">Short push</option>
<option value="SS">Double-Short push</option>
<option value="SSS">Triple-Short push</option>
<option value="L">Long push</option>
<option value="SL">Short-Long push</option>
<option value="LS">Long-Short push</option>
</options>
</state>
</channel-type>
<channel-type id="eventCount">
<item-type>Number</item-type>
<label>Event Count</label>
<description>Event Count</description>
<state pattern="%d" readOnly="true">
</state>
</channel-type>
<channel-type id="sensorTemp">
<item-type>Number:Temperature</item-type>
<label>Temperature</label>
<description>Temperature from the sensor</description>
<category>Temperature</category>
<tags>
<tag>CurrentTemperature</tag>
</tags>
<state readOnly="true" pattern="%f %unit%">
</state>
</channel-type>
<channel-type id="sensorExtTemp">
<item-type>Number:Temperature</item-type>
<label>Temperature</label>
<description>Temperature from the external sensor</description>
<category>Temperature</category>
<tags>
<tag>CurrentTemperature</tag>
</tags>
<state readOnly="true" pattern="%f %unit%">
</state>
</channel-type>
<channel-type id="sensorExtHum">
<item-type>Number:Dimensionless</item-type>
<label>Humidity</label>
<description>Relative humidity in % (0..100%) from external sensor</description>
<category>Humidity</category>
<tags>
<tag>CurrentHumidity</tag>
</tags>
<state readOnly="true" pattern="%f %unit%">
</state>
</channel-type>
<channel-type id="sensorHumidity">
<item-type>Number:Dimensionless</item-type>
<label>Humidity</label>
<description>Relative humidity in % (0..100%)</description>
<tags>
<tag>CurrentHumidity</tag>
</tags>
<state readOnly="true" min="0" max="100" pattern="%f %unit%"/>
</channel-type>
<channel-type id="sensorFlood">
<item-type>Switch</item-type>
<label>Flood Alarm</label>
<description>ON: Indicates flood condition / water detected</description>
<state readOnly="true">
</state>
</channel-type>
<channel-type id="sensorSmoke">
<item-type>Switch</item-type>
<label>Smoke Alarm</label>
<description>ON: Indicates smoke detection</description>
<state readOnly="true">
</state>
</channel-type>
<channel-type id="sensorLux">
<item-type>Number:Illuminance</item-type>
<label>Lux</label>
<description>Brightness from the sensor (Lux)</description>
<state readOnly="true" pattern="%f %unit%">
</state>
</channel-type>
<channel-type id="sensorIllumination">
<item-type>String</item-type>
<label>Illumination</label>
<description>Current illumination: dark/twilight/bright</description>
<state readOnly="true">
<options>
<option value="dark">Dark</option>
<option value="twilight">Twilight</option>
<option value="bright">Bright</option>
</options>
</state>
</channel-type>
<channel-type id="sensorVibration">
<item-type>Switch</item-type>
<label>Vibration</label>
<description>ON: Vibration detected</description>
<state readOnly="true">
</state>
</channel-type>
<channel-type id="sensorTilt">
<item-type>Number:Angle</item-type>
<label>Tilt</label>
<description>Tilt in degrees (requires calibration)</description>
<state readOnly="true">
</state>
</channel-type>
<channel-type id="sensorPPM">
<item-type>Number:Density</item-type>
<label>Concentration</label>
<description>Gas concentration in ppm</description>
<state readOnly="true" pattern="%d %unit%">
</state>
</channel-type>
<channel-type id="sensorValve">
<item-type>String</item-type>
<label>Valve</label>
<description>Current valve status: closed/opened/not_connected/failure/closing/opening/checking</description>
<state readOnly="true">
<options>
<option value="closed">closed</option>
<option value="opened">opened</option>
<option value="not_connected">not connected</option>
<option value="failure">failure</option>
<option value="closing">closing</option>
<option value="opening">opening</option>
<option value="checking">checking</option>
<option value="unknown">UNKNOWN</option>
</options>
</state>
</channel-type>
<channel-type id="sensorError">
<item-type>String</item-type>
<label>Last Error</label>
<description>Only valid in case of error</description>
<state readOnly="true">
</state>
</channel-type>
<channel-type id="senseKey">
<item-type>String</item-type>
<label>IR Key to Send</label>
<description>Send a defined key code</description>
</channel-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="shelly"
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="shellydevice">
<label>Shelly Device</label>
<description>A password protected or unknown device.</description>
<config-description>
<parameter name="userId" type="text" required="true">
<label>UserID</label>
<description>User ID for API access.</description>
</parameter>
<parameter name="password" type="text" required="true">
<label>Password</label>
<description>Password for API access.</description>
<context>password</context>
</parameter>
<parameter name="deviceIp" type="text" required="true">
<label>IP Address</label>
<description>IP Address of the Shelly device.</description>
<context>network-address</context>
</parameter>
</config-description>
</thing-type>
<thing-type id="shellyunknown">
<label>Unknown Shelly Device</label>
<description>This device is currently not supported</description>
</thing-type>
</thing:thing-descriptions>