diff --git a/bundles/org.openhab.binding.shelly/README.md b/bundles/org.openhab.binding.shelly/README.md
index 73cf2c7118d65..c0062412ff91f 100644
--- a/bundles/org.openhab.binding.shelly/README.md
+++ b/bundles/org.openhab.binding.shelly/README.md
@@ -35,7 +35,6 @@ Also check out the [Shelly Manager](doc/ShellyManager.md), which
| shelly1l | Shelly 1L Single Relay Switch | SHSW-L |
| 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 |
@@ -55,7 +54,7 @@ Also check out the [Shelly Manager](doc/ShellyManager.md), which
| shellybulbduo | Shelly Duo White G10 | SHBDUO-1 |
| shellycolorbulb | Shelly Duo Color G10 | SHCB-1 |
| shellyvintage | Shelly Vintage (White Mode) | SHVIN-1 |
-| shellyht | Shelly Sensor (temp+humidity) | SHHT-1 |
+| shellyht | Shelly Sensor (temperature+humidity) | SHHT-1 |
| shellyflood | Shelly Flood Sensor | SHWT-1 |
| shellysmoke | Shelly Smoke Sensor | SHSM-1 |
| shellymotion | Shelly Motion Sensor | SHMOS-01 |
@@ -69,6 +68,31 @@ Also check out the [Shelly Manager](doc/ShellyManager.md), which
| shellytrv | Shelly TRV | SHTRV-01 |
| shellydevice | A password protected Shelly device or an unknown type | |
+### Generation 2 Plus series:
+
+| thing-type | Model | Vendor ID |
+|---------------------|----------------------------------------------------------|----------------|
+| shellyplus1 | Shelly Plus 1 with 1x relay | SNSW-001X16EU |
+| shellyplus1pm | Shelly Plus 1PM with 1x relay + power meter | SNSW-001P16EU |
+| shellyplus2pm-relay | Shelly Plus 2PM with 2x relay + power meter, relay mode | SNSW-002P16EU |
+| shellyplus2pm-roller| Shelly Plus 2PM with 2x relay + power meter, roller mode | SNSW-002P16EU |
+| shellyplusi4 | Shelly Plus i4 with 4x AC input | SNSN-0024X |
+| shellyplusi4dc | Shelly Plus i4 with 4x DC input | SNSN-0D24X |
+| shellyplusht | Shelly Plus HT with temperature + humidity sensor | SNSN-0013A |
+
+### Generation 2 Pro series:
+
+| thing-type | Model | Vendor ID |
+|---------------------|----------------------------------------------------------|----------------|
+| shellypro1 | Shelly Pro 1 with 1x relay | SPSW-001XE16EU |
+| shellypro1pm | Shelly Pro 1 PM with 1x relay + power meter | SPSW-001PE16EU |
+| shellypro2-relay | Shelly Pro 2 with 2x relay, relay mode | SPSW-002XE16EU |
+| shellypro2pm-relay | Shelly Pro 2 PM with 2x relay + power meter, relay mode | SPSW-002PE16EU |
+| shellypro2pm-roller | Shelly Pro 2 PM with 2x relay + power meter, roller mode | SPSW-002PE16EU |
+| shellypro3 | Shelly Pro 3 with 3x relay (dry contacts) | SPSW-003XE16EU |
+| shellypro4pm | Shelly Pro 4 PM with 4x relay + power meter | SPSW-004PE16EU |
+
+
## Binding Configuration
The binding has the following configuration options:
@@ -156,12 +180,14 @@ Values 1-4 are selecting the corresponding favorite id in the Shelly App, 0 mean
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 persists. |
+| 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 persists. |
+| CONFIG PENDING | CONFIG PENDING description |
+| ERROR: COMM | ERROR: COMM descritpion |
`Battery powered devices:`
If the device is in sleep mode and can't be reached by the binding, the Thing will change into CONFIG_PENDING.
@@ -335,7 +361,7 @@ Refer to section [Full Example:shelly.rules](#shelly-rules) for examples how to
Depending on the device type and firmware release channels might be not available or stay with value NaN.
-### Shelly 1(thing-type: shelly1)
+### Shelly 1 (thing-type: shelly1)
|Group |Channel |Type |read-only|Description |
|----------|-------------|---------|---------|---------------------------------------------------------------------------------|
@@ -389,7 +415,6 @@ In this case the is no real measurement based on power consumption, but the Shel
| |outputName |String |yes |Logical name of this relay output as configured in the Shelly App |
| |input |Switch |yes |ON: Input/Button is powered, see General Notes on Channels |
| |button |Trigger |yes |Event trigger, see section Button Events |
-| |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 for a round minute, 1 minute ago |
| |totalKWH |Number |yes |Total energy consumption in Watts since the device powered up (resets on restart)|
@@ -469,7 +494,7 @@ The Thing id is derived from the service name, so that's the reason why the Thin
| |lastUpdate |DateTime |yes |Timestamp of the last measurement |
-### Shelly 2 - relay mode thing-type: shelly2-relay)
+### Shelly 2 - relay mode (thing-type: shelly2-relay)
|Group |Channel |Type |read-only|Description |
|----------|-------------|---------|---------|---------------------------------------------------------------------------------|
@@ -480,7 +505,6 @@ The Thing id is derived from the service name, so that's the reason why the Thin
| |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, see section Button Events |
-| |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) |
| |outputName |String |yes |Logical name of this relay output as configured in the Shelly App |
| |input |Switch |yes |ON: Input/Button is powered, see General Notes on Channels |
@@ -488,34 +512,11 @@ The Thing id is derived from the service name, so that's the reason why the Thin
| |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, see section Button Events |
-| |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 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 |
-| |event |Trigger |yes |Roller event/trigger with payload ROLLER_OPEN / ROLLER_CLOSE / ROLLER_STOP |
-| |rollerpos |Number |r/w |Roller position: 100%=open...0%=closed; gets updated when the roller stops, see Notes |
-| |rollerFav |Number |r/w |Select roller position favorite (1-4, 0=no), see Notes |
-| |state |String |yes |Roller state: open/close/stop |
-| |stopReason |String |yes |Last stop reasons: normal, safety_switch or obstacle |
-| |safety |Switch |yes |Indicates status of the Safety Switch, ON=problem detected, powered off |
-|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 |
-
-`Note: The Roller should be calibrated using the device Web UI or Shelly App, otherwise the position can .`
-
-The roller positioning calibration has to be performed using the Shelly Web UI or App before the position can be set in percent.
-Refer to [Smartify Roller Shutters with openHAB and Shelly](doc/UseCaseSmartRoller.md) for more information on roller integration.
-
### Shelly 2.5 - relay mode (thing-type:shelly25-relay)
The Shelly 2.5 includes 2 meters, one for each channel.
@@ -605,14 +606,16 @@ Using the Thing configuration option `brightnessAutoOn` you could decide if the
|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 |
+|status1 |input |Switch |yes |State of Input 1 |
| |button |Trigger |yes |Event trigger: Event trigger, see section Button Events |
| |lastEvent |String |yes |S/SS/SSS for 1/2/3x Shortpush or L for Longpush |
| |eventCount |Number |yes |Counter gets incremented every time the device issues a button event. |
+|status2 | | | |Same for Input 2 |
+|status3 | | | |Same for Input 3 |
+
+Channels lastEvent and eventCount are only available if input type is set to momentary button
-### Shelly UNI - Low voltage sensor/actor: shellyuni)
+### Shelly UNI (thing-type: shellyuni)
|Group |Channel |Type |read-only|Description |
|----------|-------------|---------|---------|----------------------------------------------------------------------------|
@@ -659,7 +662,7 @@ Beside channel `hsb` the binding also offers the `white` channel (hsb as only RG
Or control each color separately with channels `red`, `blue`, `green` (those are advanced channels).
-#### Shelly Duo (thing-type: shellybulbduo)
+### Shelly Duo (thing-type: shellybulbduo)
This information applies to the Shelly Duo-1 as well as the Duo White for the G10 socket.
@@ -690,7 +693,7 @@ This information applies to the Shelly Duo-1 as well as the Duo White for the G1
| |totalKWH |Number |yes |Total energy consumption in kWh since the device powered up (resets on restart)|
| |lastUpdate |DateTime |yes |Timestamp of the last measurement |
-## Shelly Duo Color (thing-type: shellyduocolor-color)
+### Shelly Duo Color (thing-type: shellyduocolor-color)
|Group |Channel |Type |read-only|Description |
|----------|-------------|---------|---------|-----------------------------------------------------------------------|
@@ -719,7 +722,7 @@ Using the Thing configuration option `brightnessAutoOn` you could decide if the
`false`: Brightness will be set, but output stays unchanged so light will not be switched on when it's currently off.
-## Shelly Duo RGBW Color Bulb (thing-type: shellycolorbulb)
+### Shelly Duo RGBW Color Bulb (thing-type: shellycolorbulb)
|Group |Channel |Type |read-only|Description |
|----------|-------------|---------|---------|-----------------------------------------------------------------------|
@@ -906,7 +909,7 @@ You should calibrate the valve using the device Web UI or Shelly App before star
|control |targetTemp |Number |no |Temperature in °C: 4=Low/Min; 5..30=target temperature;31=Hi/Max |
| |position |Dimmer |no |Set valve to manual mode (0..100%) disables auto-temp) |
| |mode |String |no |Switch between manual and automatic mode |
-| |selectedProfile|String |no |Select profile: Profile name or 0=disable, 1-n: profile index |
+| |selectedProfile|String |no |Select profile Id: "0"=disable, "1"-"n": profile index |
| |boost |Number |no |Enable/disable boost mode (full heating power) |
| |boostTimer |Number |no |Number of minutes to heat at full power while boost mode is enabled |
| |schedule |Switch |yes |ON: Schedule is active |
@@ -964,6 +967,244 @@ You should calibrate the valve using the device Web UI or Shelly App before star
|battery |batteryLevel |Number |yes |Battery Level in % |
| |batteryAlert |Switch |yes |Low battery alert |
+## Shelly Plus Series
+
+### Shelly Plus 1 (thing-type: shellyplus1)
+
+|Group |Channel |Type |read-only|Description |
+|----------|-------------|---------|---------|---------------------------------------------------------------------------------|
+|relay |output |Switch |r/w |Relay #1: Controls the relay's output channel (on/off) |
+| |outputName |String |yes |Logical name of this relay output as configured in the Shelly App |
+| |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, see section Button Events |
+
+### Shelly Plus 1PM (thing-type: shellyplus1pm)
+
+|Group |Channel |Type |read-only|Description |
+|----------|-------------|---------|---------|---------------------------------------------------------------------------------|
+|relay |output |Switch |r/w |Relay #1: Controls the relay's output channel (on/off) |
+| |outputName |String |yes |Logical name of this relay output as configured in the Shelly App |
+| |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, see section Button Events |
+|meter |currentWatts |Number |yes |Current power consumption in Watts |
+| |lastPower1 |Number |yes |Energy consumption 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 Plus 2PM - relay mode (thing-type: shellyplus2pm-relay)
+
+|Group |Channel |Type |read-only|Description |
+|----------|-------------|---------|---------|---------------------------------------------------------------------------------|
+|relay1 |output |Switch |r/w |Relay #1: Controls the relay's output channel (on/off) |
+| |outputName |String |yes |Logical name of this relay output as configured in the Shelly App |
+| |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, see section Button Events |
+|meter1 |currentWatts |Number |yes |Current power consumption in Watts |
+| |lastPower1 |Number |yes |Energy consumption 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 |
+|relay2 |output |Switch |r/w |Relay #2: Controls the relay's output channel (on/off) |
+| |outputName |String |yes |Logical name of this relay output as configured in the Shelly App |
+| |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, see section Button Events |
+|meter2 |currentWatts |Number |yes |Current power consumption in Watts |
+| |lastPower1 |Number |yes |Energy consumption 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 Plus 2PM - roller mode (thing-type: shellyplus2pm-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) |
+| |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 |
+| |safety |Switch |yes |Indicates status of the Safety Switch, ON=problem detected, powered off |
+| |event |Trigger |yes |Roller event/trigger with payload ROLLER_OPEN / ROLLER_CLOSE / ROLLER_STOP |
+|meter | | | |See group meter description |
+
+The roller positioning calibration has to be performed using the Shelly Web UI or App before the position can be set in percent.
+Refer to [Smartify Roller Shutters with openHAB and Shelly](doc/UseCaseSmartRoller.md) for more information on roller integration.
+
+### Shelly Plus i4, i4DC (thing-types: shellyplusi4, shellyplusi4dc)
+
+|Group |Channel |Type |read-only|Description |
+|----------|-------------|---------|---------|-----------------------------------------------------------------------|
+|status1 |input |Switch |yes |State of Input 1 |
+| |button |Trigger |yes |Event trigger: Event trigger, see section Button Events |
+| |lastEvent |String |yes |S/SS/SSS for 1/2/3x Shortpush or L for Longpush |
+| |eventCount |Number |yes |Counter gets incremented every time the device issues a button event. |
+|status2 | | | |Same for Input 2 |
+|status3 | | | |Same for Input 3 |
+|status4 | | | |Same for Input 4 |
+
+Channels lastEvent and eventCount are only available if input type is set to momentary button
+
+### Shelly Plus HT (thing-type: shellyplusht)
+
+|Group |Channel |Type |read-only|Description |
+|----------|-------------|---------|---------|-----------------------------------------------------------------------|
+|sensors |temperature |Number |yes |Temperature, unit is reported by tempUnit |
+| |humidity |Number |yes |Relative humidity in % |
+| |lastUpdate |DateTime |yes |Timestamp of the last update (any sensor value changed) |
+|battery |batteryLevel |Number |yes |Battery Level in % |
+| |lowBattery |Switch |yes |Low battery alert (< 20%) |
+
+
+## Shelly Pro Series
+
+### Shelly Pro 1 (thing-type: shellypro1)
+
+|Group |Channel |Type |read-only|Description |
+|----------|-------------|---------|---------|---------------------------------------------------------------------------------|
+|relay |output |Switch |r/w |Controls the relay's output channel (on/off) |
+| |outputName |String |yes |Logical name of this relay output as configured in the Shelly App |
+| |input1 |Switch |yes |ON: Input/Button for input 1 is powered, see general notes on channels |
+| |button1 |Trigger |yes |Event trigger, see section Button Events |
+| |lastEvent1 |String |yes |Last event type (S/SS/SSS/L) for input 1 |
+| |eventCount1 |Number |yes |Counter gets incremented every time the device issues a button event. |
+| |input2 |Switch |yes |ON: Input/Button for channel 2 is powered, see general notes on channels |
+| |button2 |Trigger |yes |Event trigger, see section Button Events |
+| |lastEvent2 |String |yes |Last event type (S/SS/SSS/L) for input 2 |
+| |eventCount2 |Number |yes |Counter gets incremented every time the device issues a button event. |
+| |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 |
+
+### Shelly Pro 1 PM (thing-type: shellypro1pm)
+
+|Group |Channel |Type |read-only|Description |
+|----------|-------------|---------|---------|---------------------------------------------------------------------------------|
+|relay |output |Switch |r/w |Controls the relay's output channel (on/off) |
+| |outputName |String |yes |Logical name of this relay output as configured in the Shelly App |
+| |input1 |Switch |yes |ON: Input/Button for input 1 is powered, see general notes on channels |
+| |button1 |Trigger |yes |Event trigger, see section Button Events |
+| |lastEvent1 |String |yes |Last event type (S/SS/SSS/L) for input 1 |
+| |eventCount1 |Number |yes |Counter gets incremented every time the device issues a button event. |
+| |input2 |Switch |yes |ON: Input/Button for channel 2 is powered, see general notes on channels |
+| |button2 |Trigger |yes |Event trigger, see section Button Events |
+| |lastEvent2 |String |yes |Last event type (S/SS/SSS/L) for input 2 |
+| |eventCount2 |Number |yes |Counter gets incremented every time the device issues a button event. |
+| |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 |
+|meter |currentWatts |Number |yes |Current power consumption in Watts |
+| |lastPower1 |Number |yes |Energy consumption 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 Pro 2 (thing-type: shellypro2-relay)
+
+|Group |Channel |Type |read-only|Description |
+|----------|-------------|---------|---------|---------------------------------------------------------------------------------|
+|relay1 |output |Switch |r/w |Relay #1: Controls the relay's output channel (on/off) |
+| |outputName |String |yes |Logical name of this relay output as configured in the Shelly App |
+| |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, see section Button Events |
+|relay2 |output |Switch |r/w |Relay #2: Controls the relay's output channel (on/off) |
+| |outputName |String |yes |Logical name of this relay output as configured in the Shelly App |
+| |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, see section Button Events |
+
+
+### Shelly Pro 2 PM - relay mode (thing-type: shellypro2pm-relay)
+
+|Group |Channel |Type |read-only|Description |
+|----------|-------------|---------|---------|---------------------------------------------------------------------------------|
+|relay1 |output |Switch |r/w |Relay #1: Controls the relay's output channel (on/off) |
+| |outputName |String |yes |Logical name of this relay output as configured in the Shelly App |
+| |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, see section Button Events |
+|relay2 |output |Switch |r/w |Relay #2: Controls the relay's output channel (on/off) |
+| |outputName |String |yes |Logical name of this relay output as configured in the Shelly App |
+| |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, see section Button Events |
+|meter |currentWatts |Number |yes |Current power consumption in Watts |
+| |lastPower1 |Number |yes |Energy consumption 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 Pro 2 PM - roller mode (thing-type: shellypro2pm-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) |
+| |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 |
+| |safety |Switch |yes |Indicates status of the Safety Switch, ON=problem detected, powered off |
+| |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 |Energy consumption 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 Pro 3 (thing-type: shellypro3)
+
+|Group |Channel |Type |read-only|Description |
+|----------|-------------|---------|---------|---------------------------------------------------------------------------------|
+|relay1 |output |Switch |r/w |Relay #1: Controls the relay's output channel (on/off) |
+| |outputName |String |yes |Logical name of this relay output as configured in the Shelly App |
+| |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, see section Button Events |
+|relay2 |output |Switch |r/w |Relay #2: Controls the relay's output channel (on/off) |
+| |outputName |String |yes |Logical name of this relay output as configured in the Shelly App |
+| |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, see section Button Events |
+|relay3 |output |Switch |r/w |Relay #3: Controls the relay's output channel (on/off) |
+| |outputName |String |yes |Logical name of this relay output as configured in the Shelly App |
+| |input |Switch |yes |ON: Input/Button is powered, see General Notes on Channels |
+| |autoOn |Number |r/w |Relay #3: Sets a timer to turn the device ON after every OFF command; in seconds|
+| |autoOff |Number |r/w |Relay #3: Sets a timer to turn the device OFF after every ON command; in seconds|
+| |timerActive |Switch |yes |Relay #3: ON: An auto-on/off timer is active |
+| |button |Trigger |yes |Relay #3: Event trigger, see section Button Events |
+
+### Shelly Pro 4PM (thing-type: shelly4pro)
+|Group |Channel |Type |read-only|Description |
+|----------|-------------|---------|---------|---------------------------------------------------------------------------------|
+|relay1 |output |Switch |r/w |Relay #1: Controls the relay's output channel (on/off) |
+| |outputName |String |yes |Logical name of this relay output as configured in the Shelly App |
+| |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, see section Button Events |
+
## Full Example
diff --git a/bundles/org.openhab.binding.shelly/pom.xml b/bundles/org.openhab.binding.shelly/pom.xml
index f2434fbcdab9d..861ab67bfba85 100644
--- a/bundles/org.openhab.binding.shelly/pom.xml
+++ b/bundles/org.openhab.binding.shelly/pom.xml
@@ -12,6 +12,15 @@
org.openhab.binding.shelly
- openHAB Add-ons :: Bundles :: Shelly Binding
+ openHAB Add-ons :: Bundles :: Shelly Binding Gen1+2
+
+
+
+ org.eclipse.jetty.websocket
+ websocket-server
+ 9.4.46.v20220331
+ compile
+
+
diff --git a/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/ShellyBindingConstants.java b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/ShellyBindingConstants.java
index c9ae998c362e0..47abd04089cac 100755
--- a/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/ShellyBindingConstants.java
+++ b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/ShellyBindingConstants.java
@@ -35,18 +35,23 @@ public class ShellyBindingConstants {
public static final String BINDING_ID = "shelly";
public static final String SYSTEM_ID = "system";
- public static final Set SUPPORTED_THING_TYPES_UIDS = Collections
- .unmodifiableSet(Stream.of(THING_TYPE_SHELLY1, THING_TYPE_SHELLY1L, 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_SHELLYPLUGU1, THING_TYPE_SHELLYUNI, THING_TYPE_SHELLYDIMMER,
- THING_TYPE_SHELLYDIMMER2, THING_TYPE_SHELLYIX3, THING_TYPE_SHELLYBULB, THING_TYPE_SHELLYDUO,
- THING_TYPE_SHELLYVINTAGE, THING_TYPE_SHELLYDUORGBW, THING_TYPE_SHELLYRGBW2_COLOR,
- THING_TYPE_SHELLYRGBW2_WHITE, THING_TYPE_SHELLYHT, THING_TYPE_SHELLYTRV, 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_SHELLYBUTTON2, THING_TYPE_SHELLMOTION, THING_TYPE_SHELLMOTION,
- THING_TYPE_SHELLYPROTECTED, THING_TYPE_SHELLYUNKNOWN).collect(Collectors.toSet()));
+ public static final Set SUPPORTED_THING_TYPES_UIDS = Collections.unmodifiableSet(Stream
+ .of(THING_TYPE_SHELLY1, THING_TYPE_SHELLY1L, THING_TYPE_SHELLY1PM, THING_TYPE_SHELLYEM,
+ THING_TYPE_SHELLY3EM, THING_TYPE_SHELLY2_RELAY, THING_TYPE_SHELLY25_RELAY,
+ THING_TYPE_SHELLY25_ROLLER, THING_TYPE_SHELLY4PRO, THING_TYPE_SHELLYPLUG, THING_TYPE_SHELLYPLUGS,
+ THING_TYPE_SHELLYPLUGU1, THING_TYPE_SHELLYUNI, THING_TYPE_SHELLYDIMMER, THING_TYPE_SHELLYDIMMER2,
+ THING_TYPE_SHELLYIX3, THING_TYPE_SHELLYBULB, THING_TYPE_SHELLYDUO, THING_TYPE_SHELLYVINTAGE,
+ THING_TYPE_SHELLYDUORGBW, THING_TYPE_SHELLYRGBW2_COLOR, THING_TYPE_SHELLYRGBW2_WHITE,
+ THING_TYPE_SHELLYHT, THING_TYPE_SHELLYTRV, 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_SHELLYBUTTON2,
+ THING_TYPE_SHELLMOTION, THING_TYPE_SHELLMOTION, THING_TYPE_SHELLYPLUS1, THING_TYPE_SHELLYPLUS1PM,
+ THING_TYPE_SHELLYPLUS2PM_RELAY, THING_TYPE_SHELLYPLUS2PM_ROLLER, THING_TYPE_SHELLYPRO1,
+ THING_TYPE_SHELLYPRO1PM, THING_TYPE_SHELLYPRO2_RELAY, THING_TYPE_SHELLYPRO2PM_RELAY,
+ THING_TYPE_SHELLYPRO2PM_ROLLER, THING_TYPE_SHELLYPRO3, THING_TYPE_SHELLYPRO4PM,
+ THING_TYPE_SHELLYPLUSI4, THING_TYPE_SHELLYPLUSI4DC, THING_TYPE_SHELLYPLUSHT,
+ THING_TYPE_SHELLYPLUSPLUGUS, THING_TYPE_SHELLYPROTECTED, THING_TYPE_SHELLYUNKNOWN)
+ .collect(Collectors.toSet()));
// Thing Configuration Properties
public static final String CONFIG_DEVICEIP = "deviceIp";
@@ -218,6 +223,7 @@ public class ShellyBindingConstants {
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+
public static final String SHELLY_API_FW_110 = "v1.10"; // FW 1.10 or newer detected, activates some add feature
+ public static final String SHELLY2_API_MIN_FWVERSION = "v0.10.2"; // Gen 2 minimum FW
// Alarm types/messages
public static final String ALARM_TYPE_NONE = "NONE";
@@ -238,7 +244,8 @@ public class ShellyBindingConstants {
public static final String EVENT_TYPE_SENSORDATA = "report";
// URI for the EventServlet
- public static final String SHELLY_CALLBACK_URI = "/shelly/event";
+ public static final String SHELLY1_CALLBACK_URI = "/shelly/event";
+ public static final String SHELLY2_CALLBACK_URI = "/shelly/wsevent";
public static final int DIM_STEPSIZE = 5;
diff --git a/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api/ShellyApiException.java b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api/ShellyApiException.java
index 166c7b85e5a86..3a7294b56a482 100644
--- a/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api/ShellyApiException.java
+++ b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api/ShellyApiException.java
@@ -12,7 +12,12 @@
*/
package org.openhab.binding.shelly.internal.api;
+import java.net.ConnectException;
import java.net.MalformedURLException;
+import java.net.NoRouteToHostException;
+import java.net.PortUnreachableException;
+import java.net.SocketException;
+import java.net.SocketTimeoutException;
import java.net.UnknownHostException;
import java.text.MessageFormat;
import java.util.concurrent.ExecutionException;
@@ -65,19 +70,21 @@ public ShellyApiException(ShellyApiResult result, Exception exception) {
@Override
public String toString() {
- String message = nonNullString(super.getMessage());
+ String message = nonNullString(super.getMessage()).replace("java.util.concurrent.ExecutionException: ", "")
+ .replace("java.net.", "");
String cause = getCauseClass().toString();
+ String url = apiResult.getUrl();
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());
+ message = "Invalid URL: " + url;
} else if (isTimeout()) {
- message = MessageFormat.format("Device unreachable or API Timeout ({0})", apiResult.getUrl());
- } else {
- message = MessageFormat.format("{0} ({1})", message, cause);
+ message = "API Timeout for " + url;
+ } else if (!isConnectionError()) {
+ message = message + "(" + cause + ")";
}
} else {
message = apiResult.toString();
@@ -91,21 +98,28 @@ public boolean isApiException() {
public boolean isTimeout() {
Class> extype = !isEmpty() ? getCauseClass() : null;
- return (extype != null) && ((extype == TimeoutException.class) || (extype == ExecutionException.class)
- || (extype == InterruptedException.class)
+ return (extype != null) && ((extype == TimeoutException.class) || extype == InterruptedException.class
+ || extype == SocketTimeoutException.class
|| nonNullString(getMessage()).toLowerCase().contains("timeout"));
}
- public boolean isHttpAccessUnauthorized() {
- return apiResult.isHttpAccessUnauthorized();
+ public boolean isConnectionError() {
+ Class> exType = getCauseClass();
+ return isUnknownHost() || isMalformedURL() || exType == ConnectException.class
+ || exType == SocketException.class || exType == PortUnreachableException.class
+ || exType == NoRouteToHostException.class;
}
public boolean isUnknownHost() {
- return getCauseClass() == MalformedURLException.class;
+ return getCauseClass() == UnknownHostException.class;
}
public boolean isMalformedURL() {
- return getCauseClass() == UnknownHostException.class;
+ return getCauseClass() == MalformedURLException.class;
+ }
+
+ public boolean isHttpAccessUnauthorized() {
+ return apiResult.isHttpAccessUnauthorized();
}
public boolean isJSONException() {
@@ -126,6 +140,9 @@ private static String nonNullString(@Nullable String s) {
private Class> getCauseClass() {
Throwable cause = getCause();
+ if (cause != null && cause.getClass() == ExecutionException.class) {
+ cause = cause.getCause();
+ }
if (cause != null) {
return cause.getClass();
}
diff --git a/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api/ShellyApiInterface.java b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api/ShellyApiInterface.java
index 12e99f08aecb6..6505e5bcbd6a6 100644
--- a/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api/ShellyApiInterface.java
+++ b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api/ShellyApiInterface.java
@@ -20,6 +20,7 @@
import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsDevice;
import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsLogin;
import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsStatus;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsUpdate;
import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyShortLightStatus;
import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyStatusLight;
import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyStatusRelay;
@@ -59,7 +60,7 @@ public interface ShellyApiInterface {
public void setRollerPos(int relayIndex, int position) throws ShellyApiException;
- public void setTimer(int index, String timerName, int value) throws ShellyApiException;
+ public void setAutoTimer(int index, String timerName, double value) throws ShellyApiException;
public ShellyStatusSensor getSensorStatus() throws ShellyApiException;
@@ -92,12 +93,20 @@ public interface ShellyApiInterface {
public ShellyOtaCheckResult checkForUpdate() throws ShellyApiException;
+ public ShellySettingsUpdate firmwareUpdate(String uri) throws ShellyApiException;
+
public ShellySettingsLogin getLoginSettings() throws ShellyApiException;
public ShellySettingsLogin setLoginCredentials(String user, String password) throws ShellyApiException;
public String setWiFiRecovery(boolean enable) throws ShellyApiException;
+ public boolean setWiFiRangeExtender(boolean enable) throws ShellyApiException;
+
+ public boolean setEthernet(boolean enable) throws ShellyApiException;
+
+ public boolean setBluetooth(boolean enable) throws ShellyApiException;
+
public String deviceReboot() throws ShellyApiException;
public String setDebug(boolean enabled) throws ShellyApiException;
@@ -123,4 +132,6 @@ public interface ShellyApiInterface {
public void setActionURLs() throws ShellyApiException;
public void sendIRKey(String keyCode) throws ShellyApiException, IllegalArgumentException;
+
+ public void close();
}
diff --git a/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api/ShellyDeviceProfile.java b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api/ShellyDeviceProfile.java
index c42c42cf5fe48..b1d563e596196 100644
--- a/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api/ShellyDeviceProfile.java
+++ b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api/ShellyDeviceProfile.java
@@ -112,15 +112,20 @@ public class ShellyDeviceProfile {
public ShellyDeviceProfile() {
}
- public ShellyDeviceProfile initialize(String thingType, String json) throws ShellyApiException {
+ public ShellyDeviceProfile initialize(String thingType, String jsonIn) throws ShellyApiException {
Gson gson = new Gson();
initialized = false;
initFromThingType(thingType);
+
+ String json = jsonIn;
+ if (json.contains("\"ext_temperature\":{\"0\":[{")) {
+ // Shelly UNI uses ext_temperature array, reformat to avoid GSON exception
+ json = json.replace("ext_temperature", "ext_temperature_array");
+ }
settingsJson = json;
- ShellySettingsGlobal gs = fromJson(gson, json, ShellySettingsGlobal.class);
- settings = gs; // only update when no exception
+ settings = fromJson(gson, json, ShellySettingsGlobal.class);
// General settings
name = getString(settings.name);
@@ -282,6 +287,7 @@ public String getInputSuffix(int i) {
return "";
}
+ @SuppressWarnings("null")
public boolean inButtonMode(int idx) {
if (idx < 0) {
logger.debug("{}: Invalid index {} for inButtonMode()", thingName, idx);
@@ -323,8 +329,8 @@ public boolean inButtonMode(int idx) {
}
public int getRollerFav(int id) {
- if ((id >= 0) && getBool(settings.favoritesEnabled) && (settings.favorites != null)
- && (id < settings.favorites.size())) {
+ if (id >= 0 && getBool(settings.favoritesEnabled) && settings.favorites != null
+ && id < settings.favorites.size()) {
return settings.favorites.get(id).pos;
}
return -1;
@@ -348,8 +354,11 @@ public String getValueProfile(int valveId, int profileId) {
public static String extractFwVersion(@Nullable String version) {
if (version != null) {
- // fix version e.g. 20210319-122304/v.1.10-Dimmer1-gfd4cc10 (with v.1. instead of v1.)
- String vers = version.replace("/v.1.10-", "/v1.10.0-");
+ // fix version e.g.
+ // 20210319-122304/v.1.10-Dimmer1-gfd4cc10 (with v.1. instead of v1.)
+ // 20220809-125346/v1.12-g99f7e0b (.0 in 1.12.0 missing)
+ String vers = version.replace("/v.1.10-", "/v1.10.0-") //
+ .replace("/v1.12-", "/v1.12.0");
// Extract version from string, e.g. 20210226-091047/v1.10.0-rc2-89-g623b41ec0-master
Matcher matcher = VERSION_PATTERN.matcher(vers);
diff --git a/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api/ShellyEventServlet.java b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api/ShellyEventServlet.java
index a78e2219b9625..c01c6aba614b8 100644
--- a/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api/ShellyEventServlet.java
+++ b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api/ShellyEventServlet.java
@@ -21,89 +21,86 @@
import java.util.TreeMap;
import javax.servlet.ServletException;
+import javax.servlet.annotation.WebServlet;
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.eclipse.jetty.websocket.servlet.ServletUpgradeRequest;
+import org.eclipse.jetty.websocket.servlet.ServletUpgradeResponse;
+import org.eclipse.jetty.websocket.servlet.WebSocketCreator;
+import org.eclipse.jetty.websocket.servlet.WebSocketServlet;
+import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory;
import org.openhab.binding.shelly.internal.ShellyHandlerFactory;
+import org.openhab.binding.shelly.internal.api2.Shelly2RpcSocket;
+import org.openhab.binding.shelly.internal.handler.ShellyThingTable;
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).
+ * {@link Shelly2RpcServlet} implements the WebSocket callback for Gen2 devices
*
* @author Markus Michels - Initial contribution
*/
@NonNullByDefault
+@WebServlet(name = "ShellyEventServlet", urlPatterns = { SHELLY1_CALLBACK_URI, SHELLY2_CALLBACK_URI })
@Component(service = HttpServlet.class, configurationPolicy = ConfigurationPolicy.OPTIONAL)
-public class ShellyEventServlet extends HttpServlet {
- private static final long serialVersionUID = 549582869577534569L;
+public class ShellyEventServlet extends WebSocketServlet {
+ private static final long serialVersionUID = -1210354558091063207L;
private final Logger logger = LoggerFactory.getLogger(ShellyEventServlet.class);
- private final HttpService httpService;
private final ShellyHandlerFactory handlerFactory;
+ private final ShellyThingTable thingTable;
@Activate
- public ShellyEventServlet(@Reference HttpService httpService, @Reference ShellyHandlerFactory handlerFactory,
- Map config) {
- this.httpService = httpService;
+ public ShellyEventServlet(@Reference ShellyHandlerFactory handlerFactory, @Reference ShellyThingTable thingTable) {
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);
- }
+ this.thingTable = thingTable;
+ logger.debug("Shelly EventServlet started at {} and {}", SHELLY1_CALLBACK_URI, SHELLY2_CALLBACK_URI);
}
@Deactivate
protected void deactivate() {
- httpService.unregister(SHELLY_CALLBACK_URI);
- logger.debug("ShellyEventServlet stopped");
+ logger.debug("ShellyEventServlet: Stopping");
}
+ /**
+ * Servlet handler. Shelly1: http request, Shelly2: WebSocket call
+ */
@Override
- protected void service(@Nullable HttpServletRequest request, @Nullable HttpServletResponse resp)
+ protected void service(HttpServletRequest request, HttpServletResponse resp)
throws ServletException, IOException, IllegalArgumentException {
- String path = "";
- String deviceName = "";
- String index = "";
- String type = "";
+ String path = getString(request.getRequestURI()).toLowerCase();
- if ((request == null) || (resp == null)) {
- logger.debug("request or resp must not be null!");
+ if (path.equals(SHELLY2_CALLBACK_URI)) { // Shelly2 WebSocket
+ super.service(request, resp);
return;
}
+ // Shelly1: http events, URL looks like
+ // :/shelly/event/shellyrelay-XXXXXX/relay/n?xxxxx or
+ // :/shelly/event/shellyrelay-XXXXXX/roller/n?xxxxx or
+ // :/shelly/event/shellyht-XXXXXX/sensordata?hum=53,temp=26.50
+ String deviceName = "";
+ String index = "";
+ String type = "";
try {
- path = getString(request.getRequestURI()).toLowerCase();
- String ipAddress = request.getHeader("HTTP_X_FORWARDED_FOR");
- if (ipAddress == null) {
- ipAddress = request.getRemoteAddr();
- }
+ String ipAddress = request.getRemoteAddr();
Map parameters = request.getParameterMap();
- logger.debug("CallbackServlet: {} Request from {}:{}{}?{}", request.getProtocol(), ipAddress,
+ logger.debug("ShellyEventServlet: {} 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);
+ if (!path.toLowerCase().startsWith(SHELLY1_CALLBACK_URI) || !path.contains("/event/shelly")) {
+ logger.warn("ShellyEventServlet received unknown request: path = {}", path);
return;
}
- // URL looks like
- // :/shelly/event/shellyrelay-XXXXXX/relay/n?xxxxx or
- // :/shelly/event/shellyrelay-XXXXXX/roller/n?xxxxx or
- // :/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 + "/")) {
@@ -114,8 +111,8 @@ protected void service(@Nullable HttpServletRequest request, @Nullable HttpServl
type = substringAfterLast(path, "/").toLowerCase();
}
logger.trace("{}: Process event of type type={}, index={}", deviceName, type, index);
- Map parms = new TreeMap<>();
+ Map parms = new TreeMap<>();
for (Map.Entry p : parameters.entrySet()) {
parms.put(p.getKey(), p.getValue()[0]);
@@ -129,4 +126,39 @@ protected void service(@Nullable HttpServletRequest request, @Nullable HttpServl
resp.getWriter().write("");
}
}
+
+ /*
+ * @Override
+ * public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
+ * {
+ * response.getWriter().println("HTTP GET method not implemented.");
+ * }
+ */
+ /**
+ * WebSocket: register Shelly2RpcSocket class
+ */
+ @Override
+ public void configure(@Nullable WebSocketServletFactory factory) {
+ if (factory != null) {
+ factory.getPolicy().setIdleTimeout(15000);
+ factory.setCreator(new Shelly2WebSocketCreator(thingTable));
+ factory.register(Shelly2RpcSocket.class);
+ }
+ }
+
+ public static class Shelly2WebSocketCreator implements WebSocketCreator {
+ private final Logger logger = LoggerFactory.getLogger(Shelly2WebSocketCreator.class);
+
+ private final ShellyThingTable thingTable;
+
+ public Shelly2WebSocketCreator(ShellyThingTable thingTable) {
+ this.thingTable = thingTable;
+ }
+
+ @Override
+ public Object createWebSocket(@Nullable ServletUpgradeRequest req, @Nullable ServletUpgradeResponse resp) {
+ logger.debug("WebSocket: Create socket from servlet");
+ return new Shelly2RpcSocket(thingTable, true);
+ }
+ }
}
diff --git a/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api/ShellyHttpClient.java b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api/ShellyHttpClient.java
index 1bc9a8fb7f4e0..ffb60e42054cb 100644
--- a/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api/ShellyHttpClient.java
+++ b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api/ShellyHttpClient.java
@@ -32,6 +32,7 @@
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.HttpStatus;
+import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2RpcBaseMessage;
import org.openhab.binding.shelly.internal.config.ShellyThingConfiguration;
import org.openhab.binding.shelly.internal.handler.ShellyThingInterface;
import org.slf4j.Logger;
@@ -64,7 +65,6 @@ public class ShellyHttpClient {
public ShellyHttpClient(String thingName, ShellyThingInterface thing) {
this(thingName, thing.getThingConfig(), thing.getHttpClient());
this.profile = thing.getProfile();
- profile.initFromThingType(thingName);
}
public ShellyHttpClient(String thingName, ShellyThingConfiguration config, HttpClient httpClient) {
@@ -111,8 +111,9 @@ protected String httpRequest(String uri) throws ShellyApiException {
}
return apiResult.response; // successful
} catch (ShellyApiException e) {
- if ((!e.isTimeout() && !apiResult.isHttpServerError()) && !apiResult.isNotFound() || profile.hasBattery
- || (retries == 0)) {
+ if (e.isConnectionError()
+ || (!e.isTimeout() && !apiResult.isHttpServerError()) && !apiResult.isNotFound()
+ || profile.hasBattery || (retries == 0)) {
// Sensor in sleep mode or API exception for non-battery device or retry counter expired
throw e; // non-timeout exception
}
@@ -154,6 +155,16 @@ private ShellyApiResult innerRequest(HttpMethod method, String uri, String data)
String response = contentResponse.getContentAsString().replace("\t", "").replace("\r\n", "").trim();
logger.trace("{}: HTTP Response {}: {}", thingName, contentResponse.getStatus(), response);
+ if (response.contains("\"error\":{")) { // Gen2
+ Shelly2RpcBaseMessage message = gson.fromJson(response, Shelly2RpcBaseMessage.class);
+ if (message != null && message.error != null) {
+ apiResult.httpCode = message.error.code;
+ apiResult.response = message.error.message;
+ if (getInteger(message.error.code) == HttpStatus.UNAUTHORIZED_401) {
+ apiResult.authResponse = getString(message.error.message).replaceAll("\\\"", "\"");
+ }
+ }
+ }
HttpFields headers = contentResponse.getHeaders();
String auth = headers.get(HttpHeader.WWW_AUTHENTICATE);
if (!getString(auth).isEmpty()) {
@@ -171,7 +182,7 @@ private ShellyApiResult innerRequest(HttpMethod method, String uri, String data)
}
} catch (ExecutionException | InterruptedException | TimeoutException | IllegalArgumentException e) {
ShellyApiException ex = new ShellyApiException(apiResult, e);
- if (!ex.isTimeout()) { // will be handled by the caller
+ if (!ex.isConnectionError() && !ex.isTimeout()) { // will be handled by the caller
logger.trace("{}: API call returned exception", thingName, ex);
}
throw ex;
diff --git a/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api1/Shelly1ApiJsonDTO.java b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api1/Shelly1ApiJsonDTO.java
index d968ce50fc81f..8398384245bf6 100644
--- a/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api1/Shelly1ApiJsonDTO.java
+++ b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api1/Shelly1ApiJsonDTO.java
@@ -283,6 +283,7 @@ public static class ShellySettingsWiFiAp {
public Boolean enabled;
public String ssid;
public String key;
+ public Boolean rangeExtender; // Gen2 only
}
public static class ShellySettingsWiFiNetwork {
@@ -605,6 +606,7 @@ public static class ShellySettingsGlobal {
@SerializedName("max_power")
public Double maxPower;
public Boolean calibrated;
+
public Double voltage; // AC voltage for Shelly 2.5
@SerializedName("supply_voltage")
public Long supplyVoltage; // Shelly 1PM/1L: 0=110V, 1=220V
@@ -675,6 +677,10 @@ public static class ShellySettingsGlobal {
@SerializedName("sleep_time") // Shelly Motion
public Integer sleepTime;
+
+ // Gen2
+ public Boolean ethernet;
+ public Boolean bluetooth;
}
public static class ShellySettingsAttributes {
@@ -701,7 +707,8 @@ public static class ShellySettingsStatus {
public String name; // FW 1.8: Symbolic Device name is configurable
@SerializedName("wifi_sta")
- public ShellySettingsWiFiNetwork wifiSta = new ShellySettingsWiFiNetwork();
+ public ShellySettingsWiFiNetwork wifiSta = new ShellySettingsWiFiNetwork(); // WiFi client configuration. See
+ // /settings/sta for details
public ShellyStatusCloud cloud = new ShellyStatusCloud();
public ShellyStatusMqtt mqtt = new ShellyStatusMqtt();
@@ -715,13 +722,14 @@ public static class ShellySettingsStatus {
public Integer cfgChangedCount; // FW 1.8
@SerializedName("actions_stats")
public ShellyActionsStats astats;
- public Double voltage; // Shelly 2.5
- public Integer input; // RGBW2 has no JSON array
public ArrayList relays;
- public ArrayList rollers;
- public ArrayList dimmers;
+ public Double voltage; // Shelly 2.5
+ public Integer input; // RGBW2 has no JSON array
public ArrayList inputs;
+ public ArrayList dimmers;
+ public ArrayList rollers;
+ public ArrayList lights;
public ArrayList meters;
public ArrayList emeters;
@SerializedName("ext_temperature")
@@ -743,7 +751,6 @@ public static class ShellySettingsStatus {
public ArrayList thermostats;
public ShellySettingsUpdate update = new ShellySettingsUpdate();
-
@SerializedName("ram_total")
public Long ramTotal;
@SerializedName("ram_free")
@@ -798,7 +805,6 @@ public static class ShellyShortLightStatus {
public Boolean ison; // Whether output channel is on or off
public String mode; // color or white - valid only for Bulb and RGBW2 even Dimmer returns it also
public Integer brightness; // brightness: 0.100%
-
@SerializedName("has_timer")
public Boolean hasTimer;
}
@@ -914,6 +920,7 @@ public static class ShellySensorTmp {
public static class ShellyStatusSensor {
// https://shelly-api-docs.shelly.cloud/#h-amp-t-settings
+
public static class ShellySensorHum {
public Double value; // relative humidity in %
}
@@ -964,6 +971,7 @@ public static class ShellyMotionSettings {
public static class ShellyExtTemperature {
public static class ShellyShortTemp {
+ public String hwID; // e.g. "2882379497020381",
public Double tC; // temperature in deg C
public Double tF; // temperature in deg F
}
diff --git a/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api1/Shelly1CoIoTVersion1.java b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api1/Shelly1CoIoTVersion1.java
index 9eb440a88a7e9..014349808cb32 100644
--- a/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api1/Shelly1CoIoTVersion1.java
+++ b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api1/Shelly1CoIoTVersion1.java
@@ -152,8 +152,7 @@ public boolean handleStatusUpdate(List sensorUpdates, CoIotDescrSen
toQuantityType(getDouble(s.value), DIGITS_VOLT, Units.AMPERE));
break;
case "pf":
- updateChannel(updates, rGroup, CHANNEL_EMETER_PFACTOR,
- toQuantityType(getDecimal(s.value), Units.PERCENT));
+ updateChannel(updates, rGroup, CHANNEL_EMETER_PFACTOR, getDecimal(s.value));
break;
case "position":
// work around: Roller reports 101% instead max 100
diff --git a/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api1/Shelly1CoapHandler.java b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api1/Shelly1CoapHandler.java
index bd0889b16ec43..c4077cb94e482 100644
--- a/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api1/Shelly1CoapHandler.java
+++ b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api1/Shelly1CoapHandler.java
@@ -84,8 +84,6 @@ public class Shelly1CoapHandler implements Shelly1CoapListener {
private boolean updatesRequested = false;
private int coiotPort = COIOT_PORT;
- private long coiotMessages = 0;
- private long coiotErrors = 0;
private int lastSerial = -1;
private String lastPayload = "";
private Map blkMap = new LinkedHashMap<>();
@@ -164,7 +162,7 @@ public boolean isStarted() {
@Override
public void processResponse(@Nullable Response response) {
if (response == null) {
- coiotErrors++;
+ thingHandler.incProtErrors();
return; // other device instance
}
ResponseCode code = response.getCode();
@@ -172,7 +170,7 @@ public void processResponse(@Nullable Response response) {
// error handling
logger.debug("{}: Unknown Response Code {} received, payload={}", thingName, code,
response.getPayloadString());
- coiotErrors++;
+ thingHandler.incProtErrors();
return;
}
@@ -205,14 +203,14 @@ public void processResponse(@Nullable Response response) {
String uri = "";
int serial = -1;
try {
- coiotMessages++;
+ thingHandler.incProtMessages();
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);
- coiotErrors++;
+ thingHandler.incProtErrors();
return;
}
@@ -285,7 +283,7 @@ public void processResponse(@Nullable Response response) {
}
} catch (ShellyApiException e) {
logger.debug("{}: Unable to process CoIoT message: {}", thingName, e.toString());
- coiotErrors++;
+ thingHandler.incProtErrors();
}
if (!updatesRequested) {
@@ -296,7 +294,7 @@ public void processResponse(@Nullable Response response) {
} catch (JsonSyntaxException | IllegalArgumentException | NullPointerException e) {
logger.debug("{}: Unable to process CoIoT Message for payload={}", thingName, payload, e);
resetSerial();
- coiotErrors++;
+ thingHandler.incProtErrors();
}
}
@@ -500,6 +498,7 @@ i, s.id, getString(s.valueStr).isEmpty() ? s.value : s.valueStr, sen.desc, sen.t
// Aggregate Meter Data from different Coap updates
int i = 1;
double totalCurrent = 0.0;
+ @SuppressWarnings("unused")
double totalKWH = 0.0;
boolean updateMeter = false;
while (i <= thingHandler.getProfile().numMeters) {
@@ -663,14 +662,6 @@ public synchronized void stop() {
coiotBound = false;
}
- public long getMessageCount() {
- return coiotMessages;
- }
-
- public long getErrorCount() {
- return coiotErrors;
- }
-
public void dispose() {
stop();
}
diff --git a/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api1/Shelly1HttpApi.java b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api1/Shelly1HttpApi.java
index af5d754776a8f..027d75d89bc38 100644
--- a/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api1/Shelly1HttpApi.java
+++ b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api1/Shelly1HttpApi.java
@@ -151,6 +151,7 @@ public ShellySettingsStatus getStatus() throws ShellyApiException {
String json = "";
try {
json = httpRequest(SHELLY_URL_STATUS);
+
// Dimmer2 returns invalid json type for loaderror :-(
json = json.replace("\"loaderror\":0,", "\"loaderror\":false,")
.replace("\"loaderror\":1,", "\"loaderror\":true,")
@@ -223,18 +224,23 @@ public ShellyStatusSensor getSensorStatus() throws ShellyApiException {
// SHelly H&T uses external_power, Sense uses charger
status.charger = profile.settings.externalPower != 0;
}
+ if (status.tmp != null && status.tmp.tC == null && status.tmp.value != null) { // Motion is is missing tC and tF
+ status.tmp.tC = getString(status.tmp.units).toUpperCase().equals(SHELLY_TEMP_FAHRENHEIT)
+ ? ImperialUnits.FAHRENHEIT.getConverterTo(SIUnits.CELSIUS).convert(status.tmp.value).doubleValue()
+ : status.tmp.value;
+ }
return status;
}
@Override
- public void setTimer(int index, String timerName, int value) throws ShellyApiException {
+ public void setAutoTimer(int 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 + "=" + value;
+ String uri = SHELLY_URL_SETTINGS + "/" + type + "/" + index + "?" + timerName + "=" + (int) value;
httpRequest(uri);
}
@@ -351,11 +357,27 @@ public String setApRoaming(boolean enable) throws ShellyApiException { // FW 1.1
return callApi(SHELLY_URL_SETTINGS + "?ap_roaming_enabled=" + (enable ? "true" : "false"), String.class);
}
+ @Override
+ public boolean setWiFiRangeExtender(boolean enable) throws ShellyApiException {
+ return false;
+ }
+
+ @Override
+ public boolean setEthernet(boolean enable) throws ShellyApiException {
+ return false;
+ }
+
+ @Override
+ public boolean setBluetooth(boolean enable) throws ShellyApiException {
+ return false;
+ }
+
@Override
public String resetStaCache() throws ShellyApiException { // FW 1.10+: Reset cached STA/AP list and to a rescan
return callApi("/sta_cache_reset", String.class);
}
+ @Override
public ShellySettingsUpdate firmwareUpdate(String uri) throws ShellyApiException {
return callApi("/ota?" + uri, ShellySettingsUpdate.class);
}
@@ -559,7 +581,7 @@ private void setEventUrl(boolean enabled, String... eventTypes) throws ShellyApi
// 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 + "/"
+ String callBackUrl = "http://" + config.localIp + ":" + config.localPort + SHELLY1_CALLBACK_URI + "/"
+ profile.thingName + "/" + eclass + urlParm;
String newUrl = enabled ? callBackUrl : SHELLY_NULL_URL;
String testUrl = "\"" + mkEventUrl(eventType) + "\":\"" + newUrl + "\"";
@@ -581,7 +603,7 @@ private void setEventUrl(String deviceClass, Integer index, boolean enabled, Str
throws ShellyApiException {
for (String eventType : eventTypes) {
if (profile.containsEventUrl(eventType)) {
- String callBackUrl = "http://" + config.localIp + ":" + config.localPort + SHELLY_CALLBACK_URI + "/"
+ String callBackUrl = "http://" + config.localIp + ":" + config.localPort + SHELLY1_CALLBACK_URI + "/"
+ profile.thingName + "/" + deviceClass + "/" + index + "?type=" + eventType;
String newUrl = enabled ? callBackUrl : SHELLY_NULL_URL;
String test = "\"" + mkEventUrl(eventType) + "\":\"" + callBackUrl + "\"";
@@ -713,4 +735,8 @@ public int getTimeoutErrors() {
public int getTimeoutsRecovered() {
return timeoutsRecovered;
}
+
+ @Override
+ public void close() {
+ }
}
diff --git a/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api2/Shelly2ApiClient.java b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api2/Shelly2ApiClient.java
new file mode 100644
index 0000000000000..e82495bfdccbe
--- /dev/null
+++ b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api2/Shelly2ApiClient.java
@@ -0,0 +1,551 @@
+/**
+ * Copyright (c) 2010-2022 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.api2;
+
+import static org.openhab.binding.shelly.internal.ShellyBindingConstants.CHANNEL_INPUT;
+import static org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.*;
+import static org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.*;
+import static org.openhab.binding.shelly.internal.util.ShellyUtils.*;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Random;
+
+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.ShellyDeviceProfile;
+import org.openhab.binding.shelly.internal.api.ShellyHttpClient;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyFavPos;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyInputState;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyRollerStatus;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySensorTmp;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsEMeter;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsInput;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsMeter;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsRelay;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsRoller;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsStatus;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyShortStatusRelay;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyStatusRelay;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyStatusSensor;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyStatusSensor.ShellySensorBat;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyStatusSensor.ShellySensorHum;
+import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2AuthRequest;
+import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2AuthResponse;
+import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2DeviceConfig.Shelly2DevConfigCover;
+import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2DeviceConfig.Shelly2DevConfigInput;
+import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2DeviceConfig.Shelly2DevConfigSwitch;
+import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2DeviceConfig.Shelly2GetConfigResult;
+import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2DeviceStatus.Shelly2DeviceStatusResult;
+import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2DeviceStatus.Shelly2DeviceStatusResult.Shelly2CoverStatus;
+import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2DeviceStatus.Shelly2DeviceStatusResult.Shelly2DeviceStatusHumidity;
+import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2DeviceStatus.Shelly2DeviceStatusResult.Shelly2DeviceStatusPower;
+import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2DeviceStatus.Shelly2DeviceStatusResult.Shelly2DeviceStatusTempId;
+import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2DeviceStatus.Shelly2InputStatus;
+import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2RelayStatus;
+import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2RpcBaseMessage;
+import org.openhab.binding.shelly.internal.config.ShellyThingConfiguration;
+import org.openhab.binding.shelly.internal.handler.ShellyBaseHandler;
+import org.openhab.binding.shelly.internal.handler.ShellyComponents;
+import org.openhab.binding.shelly.internal.handler.ShellyThingInterface;
+import org.openhab.core.types.State;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * {@link Shelly2ApiClient} Low level part of the RPC API
+ *
+ * @author Markus Michels - Initial contribution
+ */
+@NonNullByDefault
+public class Shelly2ApiClient extends ShellyHttpClient {
+ private final Logger logger = LoggerFactory.getLogger(Shelly2ApiClient.class);
+ protected final Random random = new Random();
+ protected final ShellyStatusRelay relayStatus = new ShellyStatusRelay();
+ protected final ShellyStatusSensor sensorData = new ShellyStatusSensor();
+ protected final ArrayList rollerStatus = new ArrayList<>();
+ protected @Nullable ShellyThingInterface thing;
+ protected @Nullable Shelly2AuthRequest authReq;
+
+ public Shelly2ApiClient(String thingName, ShellyThingInterface thing) {
+ super(thingName, thing);
+ this.thing = thing;
+ }
+
+ public Shelly2ApiClient(String thingName, ShellyThingConfiguration config, HttpClient httpClient) {
+ super(thingName, config, httpClient);
+ }
+
+ protected static final Map MAP_INMODE_BTNTYPE = new HashMap<>();
+ static {
+ MAP_INMODE_BTNTYPE.put(SHELLY2_BTNT_MOMENTARY, SHELLY_BTNT_MOMENTARY);
+ MAP_INMODE_BTNTYPE.put(SHELLY2_BTNT_FLIP, SHELLY_BTNT_TOGGLE);
+ MAP_INMODE_BTNTYPE.put(SHELLY2_BTNT_FOLLOW, SHELLY_BTNT_EDGE);
+ MAP_INMODE_BTNTYPE.put(SHELLY2_BTNT_DETACHED, SHELLY_BTNT_MOMENTARY);
+ }
+
+ protected static final Map MAP_INPUT_EVENT_TYPE = new HashMap<>();
+ static {
+ MAP_INPUT_EVENT_TYPE.put(SHELLY2_EVENT_1PUSH, SHELLY_BTNEVENT_1SHORTPUSH);
+ MAP_INPUT_EVENT_TYPE.put(SHELLY2_EVENT_2PUSH, SHELLY_BTNEVENT_2SHORTPUSH);
+ MAP_INPUT_EVENT_TYPE.put(SHELLY2_EVENT_3PUSH, SHELLY_BTNEVENT_3SHORTPUSH);
+ MAP_INPUT_EVENT_TYPE.put(SHELLY2_EVENT_LPUSH, SHELLY_BTNEVENT_LONGPUSH);
+ MAP_INPUT_EVENT_TYPE.put(SHELLY2_EVENT_LSPUSH, SHELLY_BTNEVENT_LONGSHORTPUSH);
+ MAP_INPUT_EVENT_TYPE.put(SHELLY2_EVENT_SLPUSH, SHELLY_BTNEVENT_SHORTLONGPUSH);
+ }
+
+ protected static final Map MAP_INPUT_EVENT_ID = new HashMap<>();
+ static {
+ MAP_INPUT_EVENT_ID.put(SHELLY2_EVENT_BTNUP, SHELLY_EVENT_BTN_OFF);
+ MAP_INPUT_EVENT_ID.put(SHELLY2_EVENT_BTNDOWN, SHELLY_EVENT_BTN_ON);
+ MAP_INPUT_EVENT_ID.put(SHELLY2_EVENT_1PUSH, SHELLY_EVENT_SHORTPUSH);
+ MAP_INPUT_EVENT_ID.put(SHELLY2_EVENT_2PUSH, SHELLY_EVENT_DOUBLE_SHORTPUSH);
+ MAP_INPUT_EVENT_ID.put(SHELLY2_EVENT_3PUSH, SHELLY_EVENT_TRIPLE_SHORTPUSH);
+ MAP_INPUT_EVENT_ID.put(SHELLY2_EVENT_LPUSH, SHELLY_EVENT_LONGPUSH);
+ MAP_INPUT_EVENT_ID.put(SHELLY2_EVENT_LSPUSH, SHELLY_EVENT_LONG_SHORTPUSH);
+ MAP_INPUT_EVENT_ID.put(SHELLY2_EVENT_SLPUSH, SHELLY_EVENT_SHORT_LONGTPUSH);
+ }
+
+ protected static final Map MAP_INPUT_MODE = new HashMap<>();
+ static {
+ MAP_INPUT_MODE.put(SHELLY2_RMODE_SINGLE, SHELLY_INP_MODE_ONEBUTTON);
+ MAP_INPUT_MODE.put(SHELLY2_RMODE_DUAL, SHELLY_INP_MODE_OPENCLOSE);
+ MAP_INPUT_MODE.put(SHELLY2_RMODE_DETACHED, SHELLY_INP_MODE_ONEBUTTON);
+ }
+
+ protected static final Map MAP_ROLLER_STATE = new HashMap<>();
+ static {
+ MAP_ROLLER_STATE.put(SHELLY2_RSTATE_OPEN, SHELLY_RSTATE_OPEN);
+ MAP_ROLLER_STATE.put(SHELLY2_RSTATE_CLOSED, SHELLY_RSTATE_CLOSE);
+ MAP_ROLLER_STATE.put(SHELLY2_RSTATE_OPENING, SHELLY2_RSTATE_OPENING); // Gen2-only
+ MAP_ROLLER_STATE.put(SHELLY2_RSTATE_CLOSING, SHELLY2_RSTATE_CLOSING); // Gen2-only
+ MAP_ROLLER_STATE.put(SHELLY2_RSTATE_STOPPED, SHELLY_RSTATE_STOP);
+ MAP_ROLLER_STATE.put(SHELLY2_RSTATE_CALIB, SHELLY2_RSTATE_CALIB); // Gen2-only
+ }
+
+ protected @Nullable ArrayList<@Nullable ShellySettingsRelay> fillRelaySettings(ShellyDeviceProfile profile,
+ Shelly2GetConfigResult dc) {
+ if (dc.switch0 == null) {
+ return null;
+ }
+ ArrayList<@Nullable ShellySettingsRelay> relays = new ArrayList<>();
+ addRelaySettings(relays, dc.switch0);
+ addRelaySettings(relays, dc.switch1);
+ addRelaySettings(relays, dc.switch2);
+ addRelaySettings(relays, dc.switch3);
+ return relays;
+ }
+
+ private void addRelaySettings(ArrayList<@Nullable ShellySettingsRelay> relays,
+ @Nullable Shelly2DevConfigSwitch cs) {
+ if (cs == null) {
+ return;
+ }
+
+ ShellySettingsRelay rsettings = new ShellySettingsRelay();
+ rsettings.name = cs.name;
+ rsettings.ison = false;
+ rsettings.autoOn = getBool(cs.autoOn) ? cs.autoOnDelay : 0;
+ rsettings.autoOff = getBool(cs.autoOff) ? cs.autoOffDelay : 0;
+ rsettings.hasTimer = false;
+ rsettings.btnType = mapValue(MAP_INMODE_BTNTYPE, getString(cs.mode).toLowerCase());
+ relays.add(rsettings);
+ }
+
+ protected boolean fillDeviceStatus(ShellySettingsStatus status, Shelly2DeviceStatusResult result,
+ boolean channelUpdate) throws ShellyApiException {
+ boolean updated = false;
+
+ updated |= updateInputStatus(status, result, channelUpdate);
+ updated |= updateRelayStatus(status, result.switch0, channelUpdate);
+ updated |= updateRelayStatus(status, result.switch1, channelUpdate);
+ updated |= updateRelayStatus(status, result.switch2, channelUpdate);
+ updated |= updateRelayStatus(status, result.switch3, channelUpdate);
+ updated |= updateRollerStatus(status, result.cover0, channelUpdate);
+ if (channelUpdate) {
+ updated |= ShellyComponents.updateMeters(getThing(), status);
+ }
+
+ updateHumidityStatus(sensorData, result.humidity0);
+ updateTemperatureStatus(sensorData, result.temperature0);
+ updateBatteryStatus(sensorData, result.devicepower0);
+ updated |= ShellyComponents.updateSensors(getThing(), status);
+
+ return updated;
+ }
+
+ private boolean updateRelayStatus(ShellySettingsStatus status, @Nullable Shelly2RelayStatus rs,
+ boolean channelUpdate) throws ShellyApiException {
+ if (rs == null) {
+ return false;
+ }
+ ShellyDeviceProfile profile = getProfile();
+ if (rs.id >= profile.numRelays) {
+ throw new IllegalArgumentException("Update for invalid relay index");
+ }
+
+ ShellySettingsRelay rstatus = status.relays.get(rs.id);
+ ShellyShortStatusRelay sr = relayStatus.relays.get(rs.id);
+ sr.isValid = rstatus.isValid = true;
+ sr.name = rstatus.name = status.name;
+ if (rs.output != null) {
+ sr.ison = rstatus.ison = getBool(rs.output);
+ }
+ if (getDouble(rs.timerStartetAt) > 0) {
+ int duration = (int) (now() - rs.timerStartetAt);
+ sr.timerRemaining = duration;
+ }
+ if (rs.temperature != null) {
+ status.tmp.isValid = true;
+ status.tmp.tC = rs.temperature.tC;
+ status.tmp.tF = rs.temperature.tF;
+ status.tmp.units = "C";
+ sr.temperature = getDouble(rs.temperature.tC);
+ if (status.temperature == null || getDouble(rs.temperature.tC) > status.temperature) {
+ status.temperature = sr.temperature;
+ }
+ } else {
+ status.tmp.isValid = false;
+ }
+ if (rs.voltage != null) {
+ if (status.voltage == null || rs.voltage > status.voltage) {
+ status.voltage = rs.voltage;
+ }
+ }
+ if (rs.errors != null) {
+ for (String error : rs.errors) {
+ sr.overpower = rstatus.overpower = SHELLY2_ERROR_OVERPOWER.equals(error);
+ status.overload = SHELLY2_ERROR_OVERVOLTAGE.equals(error);
+ status.overtemperature = SHELLY2_ERROR_OVERTEMP.equals(error);
+ }
+ sr.overtemperature = status.overtemperature;
+ }
+
+ ShellySettingsMeter sm = new ShellySettingsMeter();
+ ShellySettingsEMeter emeter = status.emeters.get(rs.id);
+ sm.isValid = emeter.isValid = true;
+ if (rs.apower != null) {
+ sm.power = emeter.power = rs.apower;
+ }
+ if (rs.aenergy != null) {
+ sm.total = emeter.total = rs.aenergy.total;
+ sm.counters = rs.aenergy.byMinute;
+ sm.timestamp = rs.aenergy.minuteTs;
+ }
+ if (rs.voltage != null) {
+ emeter.voltage = rs.voltage;
+ }
+ if (rs.current != null) {
+ emeter.current = rs.current;
+ }
+ if (rs.pf != null) {
+ emeter.pf = rs.pf;
+ }
+
+ // Update internal structures
+ status.relays.set(rs.id, rstatus);
+ status.meters.set(rs.id, sm);
+ status.emeters.set(rs.id, emeter);
+ relayStatus.relays.set(rs.id, sr);
+ relayStatus.meters.set(rs.id, sm);
+
+ return channelUpdate ? ShellyComponents.updateRelay((ShellyBaseHandler) getThing(), status, rs.id) : false;
+ }
+
+ protected @Nullable ArrayList<@Nullable ShellySettingsRoller> fillRollerSettings(ShellyDeviceProfile profile,
+ Shelly2GetConfigResult dc) {
+ if (dc.cover0 == null) {
+ return null;
+ }
+
+ ArrayList<@Nullable ShellySettingsRoller> rollers = new ArrayList<>();
+
+ addRollerSettings(rollers, dc.cover0);
+ fillRollerFavorites(profile, dc);
+ return rollers;
+ }
+
+ private void addRollerSettings(ArrayList<@Nullable ShellySettingsRoller> rollers,
+ @Nullable Shelly2DevConfigCover coverConfig) {
+ if (coverConfig == null) {
+ return;
+ }
+
+ ShellySettingsRoller settings = new ShellySettingsRoller();
+ settings.isValid = true;
+ settings.defaultState = coverConfig.initialState;
+ settings.inputMode = mapValue(MAP_INPUT_MODE, coverConfig.inMode);
+ settings.btnReverse = getBool(coverConfig.invertDirections) ? 1 : 0;
+ settings.swapInputs = coverConfig.swapInputs;
+ settings.maxtime = 0.0; // n/a
+ settings.maxtimeOpen = coverConfig.maxtimeOpen;
+ settings.maxtimeClose = coverConfig.maxtimeClose;
+ if (coverConfig.safetySwitch != null) {
+ settings.safetySwitch = coverConfig.safetySwitch.enable;
+ settings.safetyAction = coverConfig.safetySwitch.action;
+ }
+ if (coverConfig.obstructionDetection != null) {
+ settings.obstacleAction = coverConfig.obstructionDetection.action;
+ settings.obstacleDelay = coverConfig.obstructionDetection.holdoff.intValue();
+ settings.obstaclePower = coverConfig.obstructionDetection.powerThr;
+ }
+ rollers.add(settings);
+ }
+
+ private void fillRollerFavorites(ShellyDeviceProfile profile, Shelly2GetConfigResult dc) {
+ if (dc.sys.uiData.cover != null) {
+ String[] favorites = dc.sys.uiData.cover.split(",");
+ profile.settings.favorites = new ArrayList<>();
+ for (int i = 0; i < favorites.length; i++) {
+ ShellyFavPos fav = new ShellyFavPos();
+ fav.pos = Integer.parseInt(favorites[i]);
+ fav.name = fav.pos + "%";
+ profile.settings.favorites.add(fav);
+ }
+ profile.settings.favoritesEnabled = profile.settings.favorites.size() > 0;
+ logger.debug("{}: Roller Favorites loaded: {}", thingName,
+ profile.settings.favoritesEnabled ? profile.settings.favorites.size() : "none");
+ }
+ }
+
+ private boolean updateRollerStatus(ShellySettingsStatus status, @Nullable Shelly2CoverStatus cs,
+ boolean updateChannels) throws ShellyApiException {
+ if (cs == null) {
+ return false;
+ }
+
+ ShellyRollerStatus rs = status.rollers.get(cs.id);
+ ShellySettingsMeter sm = status.meters.get(cs.id);
+ ShellySettingsEMeter emeter = status.emeters.get(cs.id);
+ rs.isValid = sm.isValid = emeter.isValid = true;
+ if (cs.state != null) {
+ if (!getString(rs.state).equals(cs.state)) {
+ logger.debug("{}: Roller status changed from {} to {}, updateChannels={}", thingName, rs.state,
+ mapValue(MAP_ROLLER_STATE, cs.state), updateChannels);
+ }
+ rs.state = mapValue(MAP_ROLLER_STATE, cs.state);
+ rs.calibrating = SHELLY2_RSTATE_CALIB.equals(cs.state);
+ }
+ if (cs.currentPos != null) {
+ rs.currentPos = cs.currentPos;
+ }
+ if (cs.moveStartedAt != null) {
+ rs.duration = (int) (now() - cs.moveStartedAt.longValue());
+ }
+ if (cs.temperature != null && cs.temperature.tC > getDouble(status.temperature)) {
+ status.temperature = status.tmp.tC = getDouble(cs.temperature.tC);
+ }
+ if (cs.apower != null) {
+ rs.power = sm.power = emeter.power = cs.apower;
+ }
+ if (cs.aenergy != null) {
+ sm.total = emeter.total = cs.aenergy.total;
+ sm.counters = cs.aenergy.byMinute;
+ sm.timestamp = (long) cs.aenergy.minuteTs;
+ }
+ if (cs.voltage != null) {
+ emeter.voltage = cs.voltage;
+ }
+ if (cs.current != null) {
+ emeter.current = cs.current;
+ }
+ if (cs.pf != null) {
+ emeter.pf = cs.pf;
+ }
+
+ rollerStatus.set(cs.id, rs);
+ status.rollers.set(cs.id, rs);
+ relayStatus.meters.set(cs.id, sm);
+ status.meters.set(cs.id, sm);
+ status.emeters.set(cs.id, emeter);
+
+ postAlarms(cs.errors);
+ if (rs.calibrating != null && rs.calibrating) {
+ getThing().postEvent(SHELLY_EVENT_ROLLER_CALIB, false);
+ }
+
+ return updateChannels ? ShellyComponents.updateRoller((ShellyBaseHandler) getThing(), rs, cs.id) : false;
+ }
+
+ protected void updateHumidityStatus(ShellyStatusSensor sdata, @Nullable Shelly2DeviceStatusHumidity value) {
+ if (value == null) {
+ return;
+ }
+ if (sdata.hum == null) {
+ sdata.hum = new ShellySensorHum();
+ }
+ sdata.hum.value = getDouble(value.rh);
+ }
+
+ protected void updateTemperatureStatus(ShellyStatusSensor sdata, @Nullable Shelly2DeviceStatusTempId value) {
+ if (value == null) {
+ return;
+ }
+ if (sdata.tmp == null) {
+ sdata.tmp = new ShellySensorTmp();
+ }
+ sdata.tmp.isValid = true;
+ sdata.tmp.units = SHELLY_TEMP_CELSIUS;
+ sdata.tmp.tC = value.tC;
+ sdata.tmp.tF = value.tF;
+ }
+
+ protected void updateBatteryStatus(ShellyStatusSensor sdata, @Nullable Shelly2DeviceStatusPower value) {
+ if (value == null) {
+ return;
+ }
+ if (sdata.bat == null) {
+ sdata.bat = new ShellySensorBat();
+ }
+
+ if (value.battery != null) {
+ sdata.bat.voltage = value.battery.volt;
+ sdata.bat.value = value.battery.percent;
+ }
+ if (value.external != null) {
+ sdata.charger = value.external.present;
+ }
+ }
+
+ private void postAlarms(@Nullable ArrayList<@Nullable String> errors) throws ShellyApiException {
+ if (errors != null) {
+ for (String e : errors) {
+ if (e != null) {
+ getThing().postEvent(e, false);
+ }
+ }
+ }
+ }
+
+ protected @Nullable ArrayList fillInputSettings(ShellyDeviceProfile profile,
+ Shelly2GetConfigResult dc) {
+ if (dc.input0 == null) {
+ return null; // device has no input
+ }
+
+ ArrayList inputs = new ArrayList<>();
+ addInputSettings(inputs, dc.input0);
+ addInputSettings(inputs, dc.input1);
+ addInputSettings(inputs, dc.input2);
+ addInputSettings(inputs, dc.input3);
+ return inputs;
+ }
+
+ private void addInputSettings(ArrayList inputs, @Nullable Shelly2DevConfigInput ic) {
+ if (ic == null) {
+ return;
+ }
+
+ ShellySettingsInput settings = new ShellySettingsInput();
+ settings.btnType = getString(ic.type).equalsIgnoreCase(SHELLY2_INPUTT_BUTTON) ? SHELLY_BTNT_MOMENTARY
+ : SHELLY_BTNT_EDGE;
+ inputs.add(settings);
+ }
+
+ protected boolean updateInputStatus(ShellySettingsStatus status, Shelly2DeviceStatusResult ds,
+ boolean updateChannels) throws ShellyApiException {
+ boolean updated = false;
+ updated |= addInputStatus(ds.input0, updateChannels);
+ updated |= addInputStatus(ds.input1, updateChannels);
+ updated |= addInputStatus(ds.input2, updateChannels);
+ updated |= addInputStatus(ds.input3, updateChannels);
+ status.inputs = relayStatus.inputs;
+ return updated;
+ }
+
+ private boolean addInputStatus(@Nullable Shelly2InputStatus is, boolean updateChannels) throws ShellyApiException {
+ if (is == null) {
+ return false;
+ }
+ ShellyDeviceProfile profile = getProfile();
+ if (is.id == null || is.id > profile.numInputs) {
+ logger.debug("{}: Invalid input id: {}", thingName, is.id);
+ return false;
+ }
+
+ String group = profile.getInputGroup(is.id);
+ ShellyInputState input = relayStatus.inputs.size() > is.id ? relayStatus.inputs.get(is.id)
+ : new ShellyInputState();
+ boolean updated = false;
+ input.input = getBool(is.state) ? 1 : 0; // old format Integer, new one Boolean
+ if (input.event == null && profile.inButtonMode(is.id)) {
+ input.event = "";
+ input.eventCount = 0;
+ }
+ relayStatus.inputs.set(is.id, input);
+ if (updateChannels) {
+ updated |= updateChannel(group, CHANNEL_INPUT + profile.getInputSuffix(is.id), getOnOff(getBool(is.state)));
+ }
+ return updated;
+ }
+
+ protected Shelly2RpcBaseMessage buildRequest(String method, @Nullable Object params) throws ShellyApiException {
+ Shelly2RpcBaseMessage request = new Shelly2RpcBaseMessage();
+ request.id = Math.abs(random.nextInt());
+ if (thingName.isEmpty()) {
+ int i = 1;
+ }
+ request.src = thingName;
+ request.method = !method.contains(".") ? SHELLYRPC_METHOD_CLASS_SHELLY + "." + method : method;
+ request.params = params;
+ request.auth = authReq;
+ return request;
+ }
+
+ protected Shelly2AuthRequest buildAuthRequest(Shelly2AuthResponse authParm, String user, String realm,
+ String password) throws ShellyApiException {
+ Shelly2AuthRequest authReq = new Shelly2AuthRequest();
+ authReq.username = "admin";
+ authReq.realm = realm;
+ authReq.nonce = authParm.nonce;
+ authReq.cnonce = (long) Math.floor(Math.random() * 10e8);
+ authReq.nc = authParm.nc != null ? authParm.nc : 1;
+ authReq.authType = SHELLY2_AUTHTTYPE_DIGEST;
+ authReq.algorithm = SHELLY2_AUTHALG_SHA256;
+ String ha1 = sha256(authReq.username + ":" + authReq.realm + ":" + password);
+ String ha2 = SHELLY2_AUTH_NOISE;
+ authReq.response = sha256(
+ ha1 + ":" + authReq.nonce + ":" + authReq.nc + ":" + authReq.cnonce + ":" + "auth" + ":" + ha2);
+ return authReq;
+ }
+
+ protected String mapValue(Map map, @Nullable String key) {
+ String value;
+ boolean known = key != null && !key.isEmpty() && map.containsKey(key);
+ value = known ? getString(map.get(key)) : "";
+ logger.trace("{}: API value {} was mapped to {}", thingName, key, known ? value : "UNKNOWN");
+ return value;
+ }
+
+ protected boolean updateChannel(String group, String channel, State value) throws ShellyApiException {
+ return getThing().updateChannel(group, channel, value);
+ }
+
+ protected ShellyThingInterface getThing() throws ShellyApiException {
+ ShellyThingInterface t = thing;
+ if (t != null) {
+ return t;
+ }
+ throw new ShellyApiException("Thing/profile not initialized!");
+ }
+
+ ShellyDeviceProfile getProfile() throws ShellyApiException {
+ if (thing != null) {
+ return thing.getProfile();
+ }
+ throw new ShellyApiException("Unable to get profile, thing not initialized!");
+ }
+}
diff --git a/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api2/Shelly2ApiJsonDTO.java b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api2/Shelly2ApiJsonDTO.java
new file mode 100644
index 0000000000000..ce204a8548b77
--- /dev/null
+++ b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api2/Shelly2ApiJsonDTO.java
@@ -0,0 +1,763 @@
+/**
+ * Copyright (c) 2010-2022 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.api2;
+
+import java.util.ArrayList;
+
+import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2DeviceStatus.Shelly2DeviceStatusResult;
+import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2RpcBaseMessage.Shelly2RpcMessageError;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * {@link Shelly2ApiJsonDTO} wraps the Shelly REST API and provides various low level function to access the device api
+ * (not
+ * cloud api).
+ *
+ * @author Markus Michels - Initial contribution
+ */
+public class Shelly2ApiJsonDTO {
+ public static final String SHELLYRPC_METHOD_CLASS_SHELLY = "Shelly";
+ public static final String SHELLYRPC_METHOD_CLASS_SWITCH = "Switch";
+
+ public static final String SHELLYRPC_METHOD_GETDEVCONFIG = "GetDeviceInfo";
+ public static final String SHELLYRPC_METHOD_GETSYSCONFIG = "GetSysConfig"; // only sys
+ public static final String SHELLYRPC_METHOD_GETCONFIG = "GetConfig"; // sys + components
+ public static final String SHELLYRPC_METHOD_GETSYSSTATUS = "GetSysStatus"; // only sys
+ public static final String SHELLYRPC_METHOD_GETSTATUS = "GetStatus"; // sys + components
+ public static final String SHELLYRPC_METHOD_REBOOT = "Shelly.Reboot";
+ public static final String SHELLYRPC_METHOD_RESET = "Shelly.FactoryReset";
+ public static final String SHELLYRPC_METHOD_CHECKUPD = "Shelly.CheckForUpdate";
+ public static final String SHELLYRPC_METHOD_UPDATE = "Shelly.Update";
+ public static final String SHELLYRPC_METHOD_AUTHSET = "Shelly.SetAuth";
+ public static final String SHELLYRPC_METHOD_SWITCH_STATUS = "Switch.GetStatus";
+ public static final String SHELLYRPC_METHOD_SWITCH_SET = "Switch.Set";
+ public static final String SHELLYRPC_METHOD_SWITCH_SETCONFIG = "Switch.SetConfig";
+ public static final String SHELLYRPC_METHOD_COVER_SETPOS = "Cover.GoToPosition";
+ public static final String SHELLY2_COVER_CMD_OPEN = "Open";
+ public static final String SHELLY2_COVER_CMD_CLOSE = "Close";
+ public static final String SHELLY2_COVER_CMD_STOP = "Stop";
+ public static final String SHELLYRPC_METHOD_WIFIGETCONG = "Wifi.GetConfig";
+ public static final String SHELLYRPC_METHOD_WIFISETCONG = "Wifi.SetConfig";
+ public static final String SHELLYRPC_METHOD_ETHGETCONG = "Eth.GetConfig";
+ public static final String SHELLYRPC_METHOD_ETHSETCONG = "Eth.SetConfig";
+ public static final String SHELLYRPC_METHOD_BLEGETCONG = "BLE.GetConfig";
+ public static final String SHELLYRPC_METHOD_BLESETCONG = "BLE.SetConfig";
+ public static final String SHELLYRPC_METHOD_CLOUDSET = "Cloud.SetConfig";
+ public static final String SHELLYRPC_METHOD_WSGETCONFIG = "WS.GetConfig";
+ public static final String SHELLYRPC_METHOD_WSSETCONFIG = "WS.SetConfig";
+
+ public static final String SHELLYRPC_METHOD_NOTIFYSTATUS = "NotifyStatus"; // inbound status
+ public static final String SHELLYRPC_METHOD_NOTIFYFULLSTATUS = "NotifyFullStatus"; // inbound status from bat device
+ public static final String SHELLYRPC_METHOD_NOTIFYEVENT = "NotifyEvent"; // inbound event
+
+ // Component types
+ public static final String SHELLY2_PROFILE_RELAY = "switch";
+ public static final String SHELLY2_PROFILE_ROLLER = "cover";
+
+ // Button types/modes
+ public static final String SHELLY2_BTNT_MOMENTARY = "momentary";
+ public static final String SHELLY2_BTNT_FLIP = "flip";
+ public static final String SHELLY2_BTNT_FOLLOW = "follow";
+ public static final String SHELLY2_BTNT_DETACHED = "detached";
+
+ // Input types
+ public static final String SHELLY2_INPUTT_SWITCH = "switch";
+ public static final String SHELLY2_INPUTT_BUTTON = "button";
+
+ // Switcm modes
+ public static final String SHELLY2_API_MODE_DETACHED = "detached";
+ public static final String SHELLY2_API_MODE_FOLLOW = "follow";
+
+ // Initial switch states
+ public static final String SHELLY2_API_ISTATE_ON = "on";
+ public static final String SHELLY2_API_ISTATE_OFF = "off";
+ public static final String SHELLY2_API_ISTATE_FOLLOWLAST = "restore_last";
+ public static final String SHELLY2_API_ISTATE_MATCHINPUT = "match_input";
+
+ // Cover/Roller modes
+ public static final String SHELLY2_RMODE_SINGLE = "single";
+ public static final String SHELLY2_RMODE_DUAL = "dual";
+ public static final String SHELLY2_RMODE_DETACHED = "detached";
+
+ public static final String SHELLY2_RSTATE_OPENING = "opening";
+ public static final String SHELLY2_RSTATE_OPEN = "open";
+ public static final String SHELLY2_RSTATE_CLOSING = "closing";
+ public static final String SHELLY2_RSTATE_CLOSED = "closed";
+ public static final String SHELLY2_RSTATE_STOPPED = "stopped";
+ public static final String SHELLY2_RSTATE_CALIB = "calibrating";
+
+ // Event notifications
+ public static final String SHELLY2_EVENT_BTNUP = "btn_up";
+ public static final String SHELLY2_EVENT_BTNDOWN = "btn_down";
+ public static final String SHELLY2_EVENT_1PUSH = "single_push";
+ public static final String SHELLY2_EVENT_2PUSH = "double_push";
+ public static final String SHELLY2_EVENT_3PUSH = "triple_push";
+ public static final String SHELLY2_EVENT_LPUSH = "long_push";
+ public static final String SHELLY2_EVENT_SLPUSH = "short_long_push";
+ public static final String SHELLY2_EVENT_LSPUSH = "long_short_push";
+
+ public static final String SHELLY2_EVENT_SLEEP = "sleep";
+ public static final String SHELLY2_EVENT_CFGCHANGED = "config_changed";
+ public static final String SHELLY2_EVENT_OTASTART = "ota_begin";
+ public static final String SHELLY2_EVENT_OTAPROGRESS = "ota_progress";
+ public static final String SHELLY2_EVENT_OTADONE = "ota_success";
+ public static final String SHELLY2_EVENT_WIFICONNFAILED = "sta_connect_fail";
+ public static final String SHELLY2_EVENT_WIFIDISCONNECTED = "sta_disconnected";
+
+ // Error Codes
+ public static final String SHELLY2_ERROR_OVERPOWER = "overpower";
+ public static final String SHELLY2_ERROR_OVERTEMP = "overtemp";
+ public static final String SHELLY2_ERROR_OVERVOLTAGE = "overvoltage";
+
+ // Wakeup reasons (e.g. Plus HT)
+ public static final String SHELLY2_WAKEUPO_BOOT_POWERON = "poweron";
+ public static final String SHELLY2_WAKEUPO_BOOT_RESTART = "software_restart";
+ public static final String SHELLY2_WAKEUPO_BOOT_WAKEUP = "deepsleep_wake";
+ public static final String SHELLY2_WAKEUPO_BOOT_INTERNAL = "internal";
+ public static final String SHELLY2_WAKEUPO_BOOT_UNKNOWN = "unknown";
+
+ public static final String SHELLY2_WAKEUPOCAUSE_BUTTON = "button";
+ public static final String SHELLY2_WAKEUPOCAUSE_USB = "usb";
+ public static final String SHELLY2_WAKEUPOCAUSE_PERIODIC = "periodic";
+ public static final String SHELLY2_WAKEUPOCAUSE_UPDATE = "status_update";
+ public static final String SHELLY2_WAKEUPOCAUSE_UNDEFINED = "undefined";
+
+ public class Shelly2DevConfigBle {
+ public Boolean enable;
+ }
+
+ public class Shelly2DevConfigEth {
+ public Boolean enable;
+ public String ipv4mode;
+ public String ip;
+ public String netmask;
+ public String gw;
+ public String nameserver;
+ }
+
+ public static class Shelly2DeviceSettings {
+ public String name;
+ public String id;
+ public String mac;
+ public String model;
+ public Integer gen;
+ @SerializedName("fw_id")
+ public String firmware;
+ public String ver;
+ public String app;
+ @SerializedName("auth_en")
+ public Boolean authEnable;
+ @SerializedName("auth_domain")
+ public String authDomain;
+ }
+
+ public static class Shelly2DeviceConfigAp {
+ public static class Shelly2DeviceConfigApRE {
+ public Boolean enable;
+ }
+
+ public Boolean enable;
+ public String ssid;
+ public String password;
+ @SerializedName("is_open")
+ public Boolean isOpen;
+ @SerializedName("range_extender")
+ Shelly2DeviceConfigApRE rangeExtender;
+ }
+
+ public static class Shelly2DeviceConfig {
+ public class Shelly2DeviceConfigSys {
+ public class Shelly2DeviceConfigDevice {
+ public String name;
+ public String mac;
+ @SerializedName("fw_id")
+ public String fwId;
+ public String profile;
+ @SerializedName("eco_mode")
+ public Boolean ecoMode;
+ public Boolean discoverable;
+ }
+
+ public class Shelly2DeviceConfigLocation {
+ public String tz;
+ public Double lat;
+ public Double lon;
+ }
+
+ public class Shelly2DeviceConfigSntp {
+ public String server;
+ }
+
+ public class Shelly2DeviceConfigSleep {
+ @SerializedName("wakeup_period")
+ public Integer wakeupPeriod;
+ }
+
+ public class Shelly2DeviceConfigDebug {
+ public class Shelly2DeviceConfigDebugMqtt {
+ public Boolean enable;
+ }
+
+ public class Shelly2DeviceConfigDebugWebSocket {
+ public Boolean enable;
+ }
+
+ public class Shelly2DeviceConfigDebugUdp {
+ public String addr;
+ }
+
+ public Shelly2DeviceConfigDebugMqtt mqtt;
+ public Shelly2DeviceConfigDebugWebSocket websocket;
+ public Shelly2DeviceConfigDebugUdp udp;
+ }
+
+ public class Shelly2DeviceConfigUiData {
+ public String cover; // hold comma seperated list of roller favorites
+ }
+
+ public class Shelly2DeviceConfigRpcUdp {
+ @SerializedName("dst_addr")
+ public String dstAddr;
+ @SerializedName("listenPort")
+ public String listenPort;
+ }
+
+ @SerializedName("cfg_rev")
+ public Integer cfgRevision;
+ public Shelly2DeviceConfigDevice device;
+ public Shelly2DeviceConfigLocation location;
+ public Shelly2DeviceConfigSntp sntp;
+ public Shelly2DeviceConfigSleep sleep;
+ public Shelly2DeviceConfigDebug debug;
+ @SerializedName("ui_data")
+ public Shelly2DeviceConfigUiData uiData;
+ @SerializedName("rpc_udp")
+ public Shelly2DeviceConfigRpcUdp rpcUdp;
+ }
+
+ public class Shelly2DevConfigInput {
+ public String id;
+ public String name;
+ public String type;
+ public Boolean invert;
+ @SerializedName("factory_reset")
+ public Boolean factoryReset;
+ }
+
+ public class Shelly2DevConfigSwitch {
+ public String id;
+ public String name;
+
+ @SerializedName("in_mode")
+ public String mode;
+
+ @SerializedName("initial_state")
+ public String initialState;
+ @SerializedName("auto_on")
+ public Boolean autoOn;
+ @SerializedName("auto_on_delay")
+ public Double autoOnDelay;
+ @SerializedName("auto_off")
+ public Boolean autoOff;
+ @SerializedName("auto_off_delay")
+ public Double autoOffDelay;
+ @SerializedName("power_limit")
+ public Integer powerLimit;
+ @SerializedName("voltage_limit")
+ public Integer voltageLimit;
+ @SerializedName("current_limit")
+ public Double currentLimit;
+ }
+
+ public class Shelly2DevConfigCover {
+ public class Shelly2DeviceConfigCoverMotor {
+ @SerializedName("idle_power_thr")
+ public Double idle_powerThr;
+ }
+
+ public class Shelly2DeviceConfigCoverSafetySwitch {
+ public Boolean enable;
+ public String direction;
+ public String action;
+ @SerializedName("allowed_move")
+ public String allowedMove;
+ }
+
+ public class Shelly2DeviceConfigCoverObstructionDetection {
+ public Boolean enable;
+ public String direction;
+ public String action;
+ @SerializedName("power_thr")
+ public Integer powerThr;
+ public Double holdoff;
+ }
+
+ public String id;
+ public String name;
+ public Shelly2DeviceConfigCoverMotor motor;
+ @SerializedName("maxtime_open")
+ public Double maxtimeOpen;
+ @SerializedName("maxtime_close")
+ public Double maxtimeClose;
+ @SerializedName("initial_state")
+ public String initialState;
+ @SerializedName("invert_directions")
+ public Boolean invertDirections;
+ @SerializedName("in_mode")
+ public String inMode;
+ @SerializedName("swap_inputs")
+ public Boolean swapInputs;
+ @SerializedName("safety_switch")
+ public Shelly2DeviceConfigCoverSafetySwitch safetySwitch;
+ @SerializedName("power_limit")
+ public Integer powerLimit;
+ @SerializedName("voltage_limit")
+ public Integer voltageLimit;
+ @SerializedName("current_limit")
+ public Double currentLimit;
+ @SerializedName("obstruction_detection")
+ public Shelly2DeviceConfigCoverObstructionDetection obstructionDetection;
+ }
+
+ public static class Shelly2GetConfigResult {
+
+ public class Shelly2DevConfigCloud {
+ public Boolean enable;
+ public String server;
+ }
+
+ public class Shelly2DevConfigMqtt {
+ public Boolean enable;
+ public String server;
+ public String user;
+ @SerializedName("topic_prefix:0")
+ public String topicPrefix;
+ @SerializedName("rpc_ntf")
+ public String rpcNtf;
+ @SerializedName("status_ntf")
+ public String statusNtf;
+ }
+
+ public Shelly2DevConfigBle ble;
+ public Shelly2DevConfigEth eth;
+ public Shelly2DevConfigCloud cloud;
+ public Shelly2DevConfigMqtt mqtt;
+ public Shelly2DeviceConfigSys sys;
+ public Shelly2DeviceConfigWiFi wifi;
+
+ @SerializedName("input:0")
+ public Shelly2DevConfigInput input0;
+ @SerializedName("input:1")
+ public Shelly2DevConfigInput input1;
+ @SerializedName("input:2")
+ public Shelly2DevConfigInput input2;
+ @SerializedName("input:3")
+ public Shelly2DevConfigInput input3;
+
+ @SerializedName("switch:0")
+ public Shelly2DevConfigSwitch switch0;
+ @SerializedName("switch:1")
+ public Shelly2DevConfigSwitch switch1;
+ @SerializedName("switch:2")
+ public Shelly2DevConfigSwitch switch2;
+ @SerializedName("switch:3")
+ public Shelly2DevConfigSwitch switch3;
+
+ @SerializedName("cover:0")
+ public Shelly2DevConfigCover cover0;
+ }
+
+ public class Shelly2DeviceConfigSta {
+ public String ssid;
+ public String password;
+ @SerializedName("is_open")
+ public Boolean isOpen;
+ public Boolean enable;
+ public String ipv4mode;
+ public String ip;
+ public String netmask;
+ public String gw;
+ public String nameserver;
+ }
+
+ public class Shelly2DeviceConfigRoam {
+ @SerializedName("rssi_thr")
+ public Integer rssiThr;
+ public Integer interval;
+ }
+
+ public class Shelly2DeviceConfigWiFi {
+ public Shelly2DeviceConfigAp ap;
+ public Shelly2DeviceConfigSta sta;
+ public Shelly2DeviceConfigSta sta1;
+ public Shelly2DeviceConfigRoam roam;
+ }
+
+ public String id;
+ public String src;
+ public Shelly2GetConfigResult result;
+ }
+
+ public static class Shelly2DeviceStatus {
+ public class Shelly2InputStatus {
+ public Integer id;
+ public Boolean state;
+ }
+
+ public static class Shelly2DeviceStatusResult {
+ public class Shelly2DeviceStatusBle {
+
+ }
+
+ public class Shelly2DeviceStatusCloud {
+ public Boolean connected;
+ }
+
+ public class Shelly2DeviceStatusMqqt {
+ public Boolean connected;
+ }
+
+ public class Shelly2CoverStatus {
+ public Integer id;
+ public String source;
+ public String state;
+ public Double apower;
+ public Double voltage;
+ public Double current;
+ public Double pf;
+ public Shelly2Energy aenergy;
+ @SerializedName("current_pos")
+ public Integer currentPos;
+ @SerializedName("target_pos")
+ public Integer targetPos;
+ @SerializedName("move_timeout")
+ public Double moveTimeout;
+ @SerializedName("move_started_at")
+ public Double moveStartedAt;
+ @SerializedName("pos_control")
+ public Boolean posControl;
+ public Shelly2DeviceStatusTemp temperature;
+ public ArrayList errors;
+ }
+
+ public class Shelly2DeviceStatusHumidity {
+ public Integer id;
+ public Double rh;
+ }
+
+ public class Shelly2DeviceStatusTempId extends Shelly2DeviceStatusTemp {
+ public Integer id;
+ }
+
+ public static class Shelly2DeviceStatusPower {
+ public static class Shelly2DeviceStatusBattery {
+ @SerializedName("V")
+ public Double volt;
+ public Double percent;
+ }
+
+ public static class Shelly2DeviceStatusCharger {
+ public Boolean present;
+ }
+
+ public Integer id;
+ public Shelly2DeviceStatusBattery battery;
+ public Shelly2DeviceStatusCharger external;
+ }
+
+ public Shelly2DeviceStatusBle ble;
+ public Shelly2DeviceStatusCloud cloud;
+ public Shelly2DeviceStatusMqqt mqtt;
+ public Shelly2DeviceStatusSys sys;
+ public Shelly2DeviceStatusSysWiFi wifi;
+
+ @SerializedName("input:0")
+ public Shelly2InputStatus input0;
+ @SerializedName("input:1")
+ public Shelly2InputStatus input1;
+ @SerializedName("input:2")
+ public Shelly2InputStatus input2;
+ @SerializedName("input:3")
+ public Shelly2InputStatus input3;
+
+ @SerializedName("switch:0")
+ public Shelly2RelayStatus switch0;
+ @SerializedName("switch:1")
+ public Shelly2RelayStatus switch1;
+ @SerializedName("switch:2")
+ public Shelly2RelayStatus switch2;
+ @SerializedName("switch:3")
+ public Shelly2RelayStatus switch3;
+
+ @SerializedName("cover:0")
+ public Shelly2CoverStatus cover0;
+
+ @SerializedName("humidity:0")
+ public Shelly2DeviceStatusHumidity humidity0;
+ @SerializedName("temperature:0")
+ public Shelly2DeviceStatusTempId temperature0;
+ @SerializedName("devicepower:0")
+ public Shelly2DeviceStatusPower devicepower0;
+ }
+
+ public class Shelly2DeviceStatusSys {
+ public class Shelly2DeviceStatusSysAvlUpdate {
+ public class Shelly2DeviceStatusSysUpdate {
+ public String version;
+ }
+
+ public Shelly2DeviceStatusSysUpdate stable;
+ public Shelly2DeviceStatusSysUpdate beta;
+ }
+
+ public class Shelly2DeviceStatusWakeup {
+ public String boot;
+ public String cause;
+ }
+
+ public String mac;
+ @SerializedName("restart_required")
+ public Boolean restartRequired;
+ public String time;
+ public Long unixtime;
+ public Long uptime;
+ @SerializedName("ram_size")
+ public Long ramSize;
+ @SerializedName("ram_free")
+ public Long ramFree;
+ @SerializedName("fs_size")
+ public Long fsSize;
+ @SerializedName("fs_free")
+ public Long fsFree;
+ @SerializedName("cfg_rev")
+ public Integer cfg_rev;
+ @SerializedName("available_updates")
+ public Shelly2DeviceStatusSysAvlUpdate availableUpdates;
+ @SerializedName("webhook_rev")
+ public Integer webHookRev;
+ @SerializedName("wakeup_reason")
+ public Shelly2DeviceStatusWakeup wakeUpReason;
+ @SerializedName("wakeup_period")
+ public Integer wakeupPeriod;
+ }
+
+ public class Shelly2DeviceStatusSysWiFi {
+ @SerializedName("sta_ip")
+ public String staIP;
+ public String status;
+ public String ssid;
+ public Integer rssi;
+ @SerializedName("ap_client_count")
+ public Integer apClientCount;
+ }
+
+ public String id;
+ public String src;
+ public Shelly2DeviceStatusResult result;
+ }
+
+ public static class Shelly2RelayStatus {
+ public Integer id;
+ public String source;
+ public Boolean output;
+ @SerializedName("timer_started_at")
+ public Double timerStartetAt;
+ @SerializedName("timer_duration")
+ public Integer timerDuration;
+ public Double apower;
+ public Double voltage;
+ public Double current;
+ public Double pf;
+ public Shelly2Energy aenergy;
+ public Shelly2DeviceStatusTemp temperature;
+ public String[] errors;
+ }
+
+ public static class Shelly2DeviceStatusTemp {
+ public Double tC;
+ public Double tF;
+ }
+
+ public static class Shelly2Energy {
+ // "switch:1":{"id":1,"aenergy":{"total":0.003,"by_minute":[0.000,0.000,0.000],"minute_ts":1619910239}}}}
+ public Double total;
+ @SerializedName("by_minute")
+ public Double[] byMinute;
+ @SerializedName("minute_ts")
+ public Long minuteTs;
+ }
+
+ public static class Shelly2ConfigParms {
+ public String name;
+ public Boolean enable;
+ public String server;
+ @SerializedName("ssl_ca")
+ public String sslCA;
+
+ // WiFi.SetConfig
+ public Shelly2DeviceConfigAp ap;
+
+ // Switch.SetConfig
+ @SerializedName("auto_on")
+ public Boolean autoOn;
+ @SerializedName("auto_on_delay")
+ public Double autoOnDelay;
+ @SerializedName("auto_off")
+ public Boolean autoOff;
+ @SerializedName("auto_off_delay")
+ public Double autoOffDelay;
+ }
+
+ public static class Shelly2RpcRequest {
+ public Integer id = 0;
+ public String method;
+
+ public static class Shelly2RpcRequestParams {
+ public Integer id = 1;
+
+ // Cover
+ public Integer pos;
+ public Boolean on;
+
+ // Shelly.SetAuth
+ public String user;
+ public String realm;
+ public String ha1;
+
+ // Shelly.Update
+ public String stage;
+ public String url;
+
+ // Cloud.SetConfig
+ public Shelly2ConfigParms config;
+
+ public Shelly2RpcRequestParams withConfig() {
+ config = new Shelly2ConfigParms();
+ return this;
+ }
+ }
+
+ public Shelly2RpcRequestParams params = new Shelly2RpcRequestParams();
+
+ public Shelly2RpcRequest() {
+ }
+
+ public Shelly2RpcRequest withMethod(String method) {
+ this.method = method;
+ return this;
+ }
+
+ public Shelly2RpcRequest withId(int id) {
+ params.id = id;
+ return this;
+ }
+
+ public Shelly2RpcRequest withPos(int pos) {
+ params.pos = pos;
+ return this;
+ }
+ }
+
+ public static class Shelly2WsConfigResponse {
+ public Integer id;
+ public String src;
+
+ public static class Shelly2WsConfigResult {
+ @SerializedName("restart_required")
+ public Boolean restartRequired;
+ }
+
+ public Shelly2WsConfigResult result;
+ }
+
+ public static class Shelly2RpcBaseMessage {
+ // Basic message format, e.g.
+ // {"id":1,"src":"localweb528","method":"Shelly.GetConfig"}
+ public class Shelly2RpcMessageError {
+ public Integer code;
+ public String message;
+ }
+
+ public Integer id;
+ public String src;
+ public String dst;
+ public String method;
+ public Object params;
+ public Object result;
+ public Shelly2AuthRequest auth;
+ public Shelly2RpcMessageError error;
+ }
+
+ public static class Shelly2RpcNotifyStatus {
+ public static class Shelly2NotifyStatus extends Shelly2DeviceStatusResult {
+ public Double ts;
+ }
+
+ public Integer id;
+ public String src;
+ public String dst;
+ public String method;
+ public Shelly2NotifyStatus params;
+ public Shelly2NotifyStatus result;
+ public Shelly2RpcMessageError error;
+ }
+
+ public static String SHELLY2_AUTHTTYPE_DIGEST = "digest";
+ public static String SHELLY2_AUTHTTYPE_STRING = "string";
+ public static String SHELLY2_AUTHALG_SHA256 = "SHA-256";
+ // = ':auth:'+HexHash("dummy_method:dummy_uri");
+ public static String SHELLY2_AUTH_NOISE = "6370ec69915103833b5222b368555393393f098bfbfbb59f47e0590af135f062";
+
+ public static class Shelly2AuthRequest {
+ public String username;
+ public Long nonce;
+ public Long cnonce;
+ public Integer nc;
+ public String realm;
+ public String algorithm;
+ public String response;
+ @SerializedName("auth_type")
+ public String authType;
+ }
+
+ public static class Shelly2AuthResponse { // on 401 message contains the auth info
+ @SerializedName("auth_type")
+ public String authType;
+ public Long nonce;
+ public Integer nc;
+ public String realm;
+ public String algorithm;
+ }
+
+ public class Shelly2NotifyEvent {
+ public Integer id;
+ public Double ts;
+ public String component;
+ public String event;
+ public String msg;
+ public Integer reason;
+ @SerializedName("cfg_rev")
+ public Integer cfgRev;
+ }
+
+ public class Shelly2NotifyEventData {
+ public Double ts;
+ public ArrayList events;
+ }
+
+ public static class Shelly2RpcNotifyEvent {
+ public Double ts;
+ Shelly2NotifyEventData params;
+ }
+}
diff --git a/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api2/Shelly2ApiRpc.java b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api2/Shelly2ApiRpc.java
new file mode 100644
index 0000000000000..07ffa7b63d6ac
--- /dev/null
+++ b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api2/Shelly2ApiRpc.java
@@ -0,0 +1,930 @@
+/**
+ * Copyright (c) 2010-2022 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.api2;
+
+import static org.openhab.binding.shelly.internal.ShellyBindingConstants.*;
+import static org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.*;
+import static org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.*;
+import static org.openhab.binding.shelly.internal.util.ShellyUtils.*;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.jetty.client.HttpClient;
+import org.eclipse.jetty.http.HttpStatus;
+import org.eclipse.jetty.websocket.api.StatusCode;
+import org.openhab.binding.shelly.internal.api.ShellyApiException;
+import org.openhab.binding.shelly.internal.api.ShellyApiInterface;
+import org.openhab.binding.shelly.internal.api.ShellyApiResult;
+import org.openhab.binding.shelly.internal.api.ShellyDeviceProfile;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyInputState;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyOtaCheckResult;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyRollerStatus;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySensorSleepMode;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsDevice;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsEMeter;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsLogin;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsMeter;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsRelay;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsStatus;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsUpdate;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsWiFiNetwork;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyShortLightStatus;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyShortStatusRelay;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyStatusLight;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyStatusRelay;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyStatusSensor;
+import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2AuthResponse;
+import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2ConfigParms;
+import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2DeviceConfig.Shelly2DeviceConfigSta;
+import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2DeviceConfig.Shelly2GetConfigResult;
+import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2DeviceConfigAp;
+import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2DeviceConfigAp.Shelly2DeviceConfigApRE;
+import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2DeviceSettings;
+import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2DeviceStatus.Shelly2DeviceStatusResult;
+import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2DeviceStatus.Shelly2DeviceStatusSys.Shelly2DeviceStatusSysAvlUpdate;
+import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2NotifyEvent;
+import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2RpcBaseMessage;
+import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2RpcNotifyEvent;
+import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2RpcNotifyStatus;
+import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2RpcNotifyStatus.Shelly2NotifyStatus;
+import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2RpcRequest;
+import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2RpcRequest.Shelly2RpcRequestParams;
+import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2WsConfigResponse;
+import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2WsConfigResponse.Shelly2WsConfigResult;
+import org.openhab.binding.shelly.internal.config.ShellyThingConfiguration;
+import org.openhab.binding.shelly.internal.handler.ShellyThingInterface;
+import org.openhab.binding.shelly.internal.handler.ShellyThingTable;
+import org.openhab.core.library.unit.SIUnits;
+import org.openhab.core.thing.ThingStatusDetail;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * {@link Shelly2ApiRpc} implements Gen2 RPC interface
+ *
+ * @author Markus Michels - Initial contribution
+ */
+@NonNullByDefault
+public class Shelly2ApiRpc extends Shelly2ApiClient implements ShellyApiInterface, Shelly2RpctInterface {
+ private final Logger logger = LoggerFactory.getLogger(Shelly2ApiRpc.class);
+ private final @Nullable ShellyThingTable thingTable;
+
+ private boolean initialized = false;
+ private boolean discovery = false;
+ private Shelly2RpcSocket rpcSocket = new Shelly2RpcSocket();
+ private Shelly2AuthResponse authInfo = new Shelly2AuthResponse();
+
+ /**
+ * Regular constructor - called by Thing handler
+ *
+ * @param thingName Symbolic thing name
+ * @param thing Thing Handler (ThingHandlerInterface)
+ */
+ public Shelly2ApiRpc(String thingName, ShellyThingTable thingTable, ShellyThingInterface thing) {
+ super(thingName, thing);
+ this.thingName = thingName;
+ this.thing = thing;
+ this.thingTable = thingTable;
+ try {
+ getProfile().initFromThingType(thing.getThingType());
+ } catch (ShellyApiException e) {
+ logger.info("{}: Shelly2 API initialization failed!", thingName, e);
+ }
+ }
+
+ /**
+ * Simple initialization - called by discovery handler
+ *
+ * @param thingName Symbolic thing name
+ * @param config Thing Configuration
+ * @param httpClient HTTP Client to be passed to ShellyHttpClient
+ */
+ public Shelly2ApiRpc(String thingName, ShellyThingConfiguration config, HttpClient httpClient) {
+ super(thingName, config, httpClient);
+ this.thingName = thingName;
+ this.thingTable = null;
+ this.discovery = true;
+ }
+
+ @Override
+ public void initialize() throws ShellyApiException {
+ if (!initialized) {
+ rpcSocket = new Shelly2RpcSocket(thingName, thingTable, config.deviceIp);
+ rpcSocket.addMessageHandler(this);
+ initialized = true;
+ } else {
+ if (rpcSocket.isConnected()) {
+ logger.debug("{}: Disconnect Rpc Socket on initialize", thingName);
+ disconnect();
+ }
+ }
+ }
+
+ @Override
+ public boolean isInitialized() {
+ return initialized;
+ }
+
+ @Override
+ public ShellyDeviceProfile getDeviceProfile(String thingType) throws ShellyApiException {
+ ShellyDeviceProfile profile = thing != null ? getProfile() : new ShellyDeviceProfile();
+
+ Shelly2GetConfigResult dc = apiRequest(SHELLYRPC_METHOD_GETCONFIG, null, Shelly2GetConfigResult.class);
+ profile.isGen2 = true;
+ profile.settingsJson = gson.toJson(dc);
+ profile.thingName = thingName;
+ profile.settings.name = profile.status.name = dc.sys.device.name;
+ profile.name = getString(profile.settings.name);
+ profile.settings.timezone = getString(dc.sys.location.tz);
+ profile.settings.discoverable = getBool(dc.sys.device.discoverable);
+ if (dc.wifi != null && dc.wifi.ap != null && dc.wifi.ap.rangeExtender != null) {
+ profile.settings.wifiAp.rangeExtender = getBool(dc.wifi.ap.rangeExtender.enable);
+ }
+ if (dc.cloud != null) {
+ profile.settings.cloud.enabled = getBool(dc.cloud.enable);
+ }
+ if (dc.mqtt != null) {
+ profile.settings.mqtt.enable = getBool(dc.mqtt.enable);
+ }
+ if (dc.sys.sntp != null) {
+ profile.settings.sntp.server = dc.sys.sntp.server;
+ }
+
+ profile.isRoller = dc.cover0 != null;
+ profile.settings.relays = fillRelaySettings(profile, dc);
+ profile.settings.inputs = fillInputSettings(profile, dc);
+ profile.settings.rollers = fillRollerSettings(profile, dc);
+
+ profile.isEMeter = true;
+ profile.numInputs = profile.settings.inputs != null ? profile.settings.inputs.size() : 0;
+ profile.numRelays = profile.settings.relays != null ? profile.settings.relays.size() : 0;
+ profile.numRollers = profile.settings.rollers != null ? profile.settings.rollers.size() : 0;
+ profile.hasRelays = profile.numRelays > 0 || profile.numRollers > 0;
+ profile.mode = "";
+ if (profile.hasRelays) {
+ profile.mode = profile.isRoller ? SHELLY_CLASS_ROLLER : SHELLY_CLASS_RELAY;
+ }
+
+ ShellySettingsDevice device = getDeviceInfo();
+ profile.settings.device = device;
+ profile.hostname = device.hostname;
+ profile.deviceType = device.type;
+ profile.mac = device.mac;
+ profile.auth = device.auth;
+ if (config.serviceName.isEmpty()) {
+ config.serviceName = getString(profile.hostname);
+ }
+ profile.fwDate = substringBefore(device.fw, "/");
+ profile.fwVersion = substringBefore(ShellyDeviceProfile.extractFwVersion(device.fw.replace("/", "/v")), "-");
+ profile.status.update.oldVersion = profile.fwVersion;
+ profile.status.hasUpdate = profile.status.update.hasUpdate = false;
+
+ if (dc.eth != null) {
+ profile.settings.ethernet = getBool(dc.eth.enable);
+ }
+ if (dc.ble != null) {
+ profile.settings.bluetooth = getBool(dc.ble.enable);
+ }
+
+ profile.settings.wifiSta = new ShellySettingsWiFiNetwork();
+ profile.settings.wifiSta1 = new ShellySettingsWiFiNetwork();
+ fillWiFiSta(dc.wifi.sta, profile.settings.wifiSta);
+ fillWiFiSta(dc.wifi.sta1, profile.settings.wifiSta1);
+
+ if (profile.hasRelays) {
+ profile.status.relays = new ArrayList<>();
+ profile.status.meters = new ArrayList<>();
+ profile.status.emeters = new ArrayList<>();
+ relayStatus.relays = new ArrayList<>();
+ relayStatus.meters = new ArrayList<>();
+ profile.numMeters = profile.isRoller ? profile.numRollers : profile.numRelays;
+ for (int i = 0; i < profile.numRelays; i++) {
+ profile.status.relays.add(new ShellySettingsRelay());
+ relayStatus.relays.add(new ShellyShortStatusRelay());
+ }
+ for (int i = 0; i < profile.numMeters; i++) {
+ profile.status.meters.add(new ShellySettingsMeter());
+ profile.status.emeters.add(new ShellySettingsEMeter());
+ relayStatus.meters.add(new ShellySettingsMeter());
+ }
+ }
+
+ if (profile.numInputs > 0) {
+ profile.status.inputs = new ArrayList<>();
+ relayStatus.inputs = new ArrayList<>();
+ for (int i = 0; i < profile.numInputs; i++) {
+ ShellyInputState input = new ShellyInputState();
+ input.input = 0;
+ input.event = "";
+ input.eventCount = 0;
+ profile.status.inputs.add(input);
+ relayStatus.inputs.add(input);
+ }
+ }
+
+ if (profile.isRoller) {
+ profile.status.rollers = new ArrayList<>();
+ for (int i = 0; i < profile.numRollers; i++) {
+ ShellyRollerStatus rs = new ShellyRollerStatus();
+ profile.status.rollers.add(rs);
+ rollerStatus.add(rs);
+ }
+ }
+
+ profile.status.dimmers = profile.isDimmer ? new ArrayList<>() : null;
+ profile.status.lights = profile.isBulb ? new ArrayList<>() : null;
+ profile.status.thermostats = profile.isTRV ? new ArrayList<>() : null;
+
+ if (profile.hasBattery) {
+ profile.settings.sleepMode = new ShellySensorSleepMode();
+ profile.settings.sleepMode.unit = "m";
+ profile.settings.sleepMode.period = dc.sys.sleep != null ? dc.sys.sleep.wakeupPeriod / 60 : 720;
+ checkSetWsCallback();
+ }
+
+ profile.initialized = true;
+ if (!discovery) {
+ getStatus(); // make sure profile.status is initialized (e.g,. relay/meter status)
+ asyncApiRequest(SHELLYRPC_METHOD_GETSTATUS); // request periodic status updates from device
+ }
+
+ return profile;
+ }
+
+ private void fillWiFiSta(@Nullable Shelly2DeviceConfigSta from, ShellySettingsWiFiNetwork to) {
+ to.enabled = from != null && !getString(from.ssid).isEmpty();
+ if (from != null) {
+ to.ssid = from.ssid;
+ to.ip = from.ip;
+ to.mask = from.netmask;
+ to.dns = from.nameserver;
+ }
+ }
+
+ private void checkSetWsCallback() throws ShellyApiException {
+ Shelly2ConfigParms wsConfig = apiRequest(SHELLYRPC_METHOD_WSGETCONFIG, null, Shelly2ConfigParms.class);
+ String url = "ws://" + config.localIp + ":" + config.localPort + "/shelly/wsevent";
+ if (!getBool(wsConfig.enable) || !url.equalsIgnoreCase(getString(wsConfig.server))) {
+ logger.debug("{}: A battery device was detected without correct callback, fix it", thingName);
+ wsConfig.enable = true;
+ wsConfig.server = url;
+ Shelly2RpcRequest request = new Shelly2RpcRequest();
+ request.id = 0;
+ request.method = SHELLYRPC_METHOD_WSSETCONFIG;
+ request.params.config = wsConfig;
+ Shelly2WsConfigResponse response = apiRequest(SHELLYRPC_METHOD_WSSETCONFIG, request.params,
+ Shelly2WsConfigResponse.class);
+ if (response.result != null && response.result.restartRequired) {
+ logger.info("{}: WebSocket callback was updated, device is restarting", thingName);
+ getThing().getApi().deviceReboot();
+ getThing().reinitializeThing();
+ }
+ }
+ }
+
+ @Override
+ public void onConnect(String deviceIp, boolean connected) {
+ if (thing == null && thingTable != null) {
+ logger.debug("{}: Get thing from thingTable", thingName);
+ thing = thingTable.getThing(deviceIp);
+ }
+ }
+
+ @Override
+ public void onNotifyStatus(Shelly2RpcNotifyStatus message) {
+ logger.debug("{}: NotifyStatus update received: {}", thingName, gson.toJson(message));
+ try {
+ if (thing == null) {
+ logger.debug("{}: No matching thing on NotifyStatus for {}, ignore (src={}, dst={}, discovery={})",
+ thingName, thingName, message.src, message.dst, discovery);
+ return;
+ }
+ if (!thing.isThingOnline() && thing.getThingStatusDetail() != ThingStatusDetail.CONFIGURATION_PENDING) {
+ logger.debug("{}: Thing is not in online state/connectable, ignore NotifyStatus", thingName);
+ return;
+ }
+
+ getThing().incProtMessages();
+ if (message.error != null) {
+ if (message.error.code == HttpStatus.UNAUTHORIZED_401 && !getString(message.error.message).isEmpty()) {
+ // Save nonce for notification
+ Shelly2AuthResponse auth = gson.fromJson(message.error.message, Shelly2AuthResponse.class);
+ if (auth != null && auth.realm == null) {
+ logger.debug("{}: Authentication data received: {}", thingName, message.error.message);
+ authInfo = auth;
+ }
+ } else {
+ logger.debug("{}: Error status received - {} {}", thingName, message.error.code,
+ message.error.message);
+ incProtErrors();
+ }
+ }
+
+ Shelly2NotifyStatus params = message.params;
+ if (params != null) {
+ if (getThing().getThingStatusDetail() != ThingStatusDetail.FIRMWARE_UPDATING) {
+ getThing().setThingOnline();
+ }
+
+ boolean updated = false;
+ ShellyDeviceProfile profile = getProfile();
+ ShellySettingsStatus status = profile.status;
+ if (params.sys != null) {
+ if (getBool(params.sys.restartRequired)) {
+ logger.warn("{}: Device requires restart to activate changes", thingName);
+ }
+ status.uptime = params.sys.uptime;
+ }
+ status.temperature = SHELLY_API_INVTEMP; // mark invalid
+ updated |= fillDeviceStatus(status, message.params, true);
+ if (getDouble(status.temperature) == SHELLY_API_INVTEMP) {
+ // no device temp available
+ status.temperature = null;
+ } else {
+ updated |= updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_ITEMP,
+ toQuantityType(getDouble(status.tmp.tC), DIGITS_NONE, SIUnits.CELSIUS));
+ }
+
+ profile.status = status;
+ if (updated) {
+ getThing().restartWatchdog();
+ }
+ }
+ } catch (ShellyApiException e) {
+ logger.debug("{}: Unable to process status update", thingName, e);
+ incProtErrors();
+ }
+ }
+
+ @Override
+ public void onNotifyEvent(Shelly2RpcNotifyEvent message) {
+ try {
+ logger.debug("{}: NotifyEvent received: {}", thingName, gson.toJson(message));
+ ShellyDeviceProfile profile = getProfile();
+
+ getThing().incProtMessages();
+ getThing().restartWatchdog();
+
+ for (Shelly2NotifyEvent e : message.params.events) {
+ switch (e.event) {
+ case SHELLY2_EVENT_BTNUP:
+ case SHELLY2_EVENT_BTNDOWN:
+ String bgroup = getProfile().getInputGroup(e.id);
+ updateChannel(bgroup, CHANNEL_INPUT + profile.getInputSuffix(e.id),
+ getOnOff(SHELLY2_EVENT_BTNDOWN.equals(getString(e.event))));
+ getThing().triggerButton(profile.getInputGroup(e.id), e.id,
+ mapValue(MAP_INPUT_EVENT_ID, e.event));
+ break;
+
+ case SHELLY2_EVENT_1PUSH:
+ case SHELLY2_EVENT_2PUSH:
+ case SHELLY2_EVENT_3PUSH:
+ case SHELLY2_EVENT_LPUSH:
+ case SHELLY2_EVENT_SLPUSH:
+ case SHELLY2_EVENT_LSPUSH:
+ if (e.id < profile.numInputs) {
+ ShellyInputState input = relayStatus.inputs.get(e.id);
+ input.event = getString(MAP_INPUT_EVENT_TYPE.get(e.event));
+ input.eventCount = getInteger(input.eventCount) + 1;
+ relayStatus.inputs.set(e.id, input);
+ profile.status.inputs.set(e.id, input);
+
+ String group = getProfile().getInputGroup(e.id);
+ updateChannel(group, CHANNEL_STATUS_EVENTTYPE + profile.getInputSuffix(e.id),
+ getStringType(input.event));
+ updateChannel(group, CHANNEL_STATUS_EVENTCOUNT + profile.getInputSuffix(e.id),
+ getDecimal(input.eventCount));
+ getThing().triggerButton(profile.getInputGroup(e.id), e.id,
+ mapValue(MAP_INPUT_EVENT_ID, e.event));
+ }
+ break;
+ case SHELLY2_EVENT_CFGCHANGED:
+ logger.debug("{}: Configuration update detected, re-initialize", thingName);
+ getThing().requestUpdates(1, true); // refresh config
+ break;
+
+ case SHELLY2_EVENT_OTASTART:
+ logger.debug("{}: Firmware update started: {}", thingName, getString(e.msg));
+ getThing().postEvent(e.event, true);
+ getThing().setThingOffline(ThingStatusDetail.FIRMWARE_UPDATING,
+ "offline.status-error-fwupgrade");
+ break;
+ case SHELLY2_EVENT_OTAPROGRESS:
+ logger.debug("{}: Firmware update in progress: {}", thingName, getString(e.msg));
+ getThing().postEvent(e.event, false);
+ break;
+ case SHELLY2_EVENT_OTADONE:
+ logger.debug("{}: Firmware update completed: {}", thingName, getString(e.msg));
+ getThing().setThingOffline(ThingStatusDetail.CONFIGURATION_PENDING,
+ "offline.status-error-restarted");
+ getThing().requestUpdates(1, true); // refresh config
+ break;
+ case SHELLY2_EVENT_SLEEP:
+ logger.debug("{}: Device went to sleep mode", thingName);
+ break;
+ case SHELLY2_EVENT_WIFICONNFAILED:
+ logger.debug("{}: WiFi connect failed, check setup, reason {}", thingName,
+ getInteger(e.reason));
+ getThing().postEvent(e.event, false);
+ break;
+ case SHELLY2_EVENT_WIFIDISCONNECTED:
+ logger.debug("{}: WiFi disconnected, reason {}", thingName, getInteger(e.reason));
+ getThing().postEvent(e.event, false);
+ break;
+ default:
+ logger.debug("{}: Event {} was not handled", thingName, e.event);
+ }
+ }
+ } catch (ShellyApiException e) {
+ logger.debug("{}: Unable to process event", thingName, e);
+ incProtErrors();
+ }
+ }
+
+ @Override
+ public void onMessage(String message) {
+ logger.debug("{}: Unexpected RPC message received: {}", thingName, message);
+ incProtErrors();
+ }
+
+ @Override
+ public void onClose(int statusCode, String reason) {
+ try {
+ logger.debug("{}: WebSocket connection closed, status = {}/{}", thingName, statusCode, getString(reason));
+ if (statusCode == StatusCode.ABNORMAL && !discovery && getProfile().alwaysOn) { // e.g. device rebooted
+ thingOffline("WebSocket connection closed abnormal");
+ }
+ } catch (ShellyApiException e) {
+ logger.debug("{}: Exception on onClose()", thingName, e);
+ incProtErrors();
+ }
+ }
+
+ @Override
+ public void onError(Throwable cause) {
+ logger.debug("{}: WebSocket error", thingName);
+ if (thing != null && thing.getProfile().alwaysOn) {
+ thingOffline("WebSocket error");
+ }
+ }
+
+ private void thingOffline(String reason) {
+ if (thing != null) { // do not reinit of battery powered devices with sleep mode
+ thing.setThingOffline(ThingStatusDetail.COMMUNICATION_ERROR, "offline.status-error-unexpected-error",
+ reason);
+ }
+ }
+
+ @Override
+ public ShellySettingsDevice getDeviceInfo() throws ShellyApiException {
+ Shelly2DeviceSettings device = callApi("/shelly", Shelly2DeviceSettings.class);
+ ShellySettingsDevice info = new ShellySettingsDevice();
+ info.hostname = getString(device.id);
+ info.fw = getString(device.firmware);
+ info.type = getString(device.model);
+ info.mac = getString(device.mac);
+ info.auth = getBool(device.authEnable);
+ info.gen = getInteger(device.gen);
+ return info;
+ }
+
+ @Override
+ public ShellySettingsStatus getStatus() throws ShellyApiException {
+ ShellyDeviceProfile profile = getProfile();
+ ShellySettingsStatus status = profile.status;
+ Shelly2DeviceStatusResult ds = apiRequest(SHELLYRPC_METHOD_GETSTATUS, null, Shelly2DeviceStatusResult.class);
+ status.time = ds.sys.time;
+ status.uptime = ds.sys.uptime;
+ status.cloud.connected = getBool(ds.cloud.connected);
+ status.mqtt.connected = getBool(ds.mqtt.connected);
+ status.wifiSta.ssid = getString(ds.wifi.ssid);
+ status.wifiSta.enabled = !status.wifiSta.ssid.isEmpty();
+ status.wifiSta.ip = getString(ds.wifi.staIP);
+ status.wifiSta.rssi = getInteger(ds.wifi.rssi);
+ status.fsFree = ds.sys.fsFree;
+ status.fsSize = ds.sys.fsSize;
+ status.discoverable = getBool(profile.settings.discoverable);
+
+ if (ds.sys.wakeupPeriod != null) {
+ profile.settings.sleepMode.period = ds.sys.wakeupPeriod / 60;
+ }
+
+ status.hasUpdate = status.update.hasUpdate = false;
+ status.update.oldVersion = getProfile().fwVersion;
+ if (ds.sys.availableUpdates != null) {
+ status.update.hasUpdate = ds.sys.availableUpdates.stable != null;
+ if (ds.sys.availableUpdates.stable != null) {
+ status.update.newVersion = "v" + getString(ds.sys.availableUpdates.stable.version);
+ }
+ if (ds.sys.availableUpdates.beta != null) {
+ status.update.betaVersion = "v" + getString(ds.sys.availableUpdates.beta.version);
+ }
+ }
+
+ if (ds.sys.wakeUpReason != null && ds.sys.wakeUpReason.boot != null) {
+ List