[shelly] Add Plus/Pro support, some bugfixes (#13439)

* Plus/Pro support and some refactoring, bugfixes

Signed-off-by: Markus Michels <markus7017@gmail.com>

* Review changes applied

Signed-off-by: Markus Michels <markus7017@gmail.com>

* review changes

Signed-off-by: Markus Michels <markus7017@gmail.com>

Signed-off-by: Markus Michels <markus7017@gmail.com>
This commit is contained in:
Markus Michels 2022-10-04 08:00:50 +02:00 committed by GitHub
parent d2dde9768f
commit 6835c278e1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
41 changed files with 4112 additions and 572 deletions

View File

@ -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:
@ -157,11 +181,13 @@ 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. |
| CONFIG PENDING | CONFIG PENDING description |
| ERROR: COMM | ERROR: COMM description |
`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.
@ -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 |
### Shelly UNI - Low voltage sensor/actor: shellyuni)
Channels lastEvent and eventCount are only available if input type is set to momentary button
### 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

View File

@ -12,6 +12,15 @@
<artifactId>org.openhab.binding.shelly</artifactId>
<name>openHAB Add-ons :: Bundles :: Shelly Binding</name>
<name>openHAB Add-ons :: Bundles :: Shelly Binding Gen1+2</name>
<dependencies>
<dependency>
<groupId>org.eclipse.jetty.websocket</groupId>
<artifactId>websocket-server</artifactId>
<version>9.4.46.v20220331</version>
<scope>compile</scope>
</dependency>
</dependencies>
</project>

View File

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

View File

@ -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 class ShellyApiException extends 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,23 +98,30 @@ public class ShellyApiException extends Exception {
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 isConnectionError() {
Class<?> exType = getCauseClass();
return isUnknownHost() || isMalformedURL() || exType == ConnectException.class
|| exType == SocketException.class || exType == PortUnreachableException.class
|| exType == NoRouteToHostException.class;
}
public boolean isUnknownHost() {
return getCauseClass() == UnknownHostException.class;
}
public boolean isMalformedURL() {
return getCauseClass() == MalformedURLException.class;
}
public boolean isHttpAccessUnauthorized() {
return apiResult.isHttpAccessUnauthorized();
}
public boolean isUnknownHost() {
return getCauseClass() == MalformedURLException.class;
}
public boolean isMalformedURL() {
return getCauseClass() == UnknownHostException.class;
}
public boolean isJSONException() {
return getCauseClass() == JsonSyntaxException.class;
}
@ -126,6 +140,9 @@ public class ShellyApiException extends Exception {
private Class<?> getCauseClass() {
Throwable cause = getCause();
if (cause != null && cause.getClass() == ExecutionException.class) {
cause = cause.getCause();
}
if (cause != null) {
return cause.getClass();
}

View File

@ -20,6 +20,7 @@ import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyRollerSt
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();
}

View File

@ -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 class ShellyDeviceProfile {
return "";
}
@SuppressWarnings("null")
public boolean inButtonMode(int idx) {
if (idx < 0) {
logger.debug("{}: Invalid index {} for inButtonMode()", thingName, idx);
@ -323,8 +329,8 @@ public class ShellyDeviceProfile {
}
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 class ShellyDeviceProfile {
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);

View File

@ -21,89 +21,86 @@ import java.util.Map;
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<String, Object> 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;
}
try {
path = getString(request.getRequestURI()).toLowerCase();
String ipAddress = request.getHeader("HTTP_X_FORWARDED_FOR");
if (ipAddress == null) {
ipAddress = request.getRemoteAddr();
}
Map<String, String[]> parameters = request.getParameterMap();
logger.debug("CallbackServlet: {} Request from {}:{}{}?{}", request.getProtocol(), ipAddress,
request.getRemotePort(), path, parameters.toString());
if (!path.toLowerCase().startsWith(SHELLY_CALLBACK_URI) || !path.contains("/event/shelly")) {
logger.warn("CallbackServlet received unknown request: path = {}", path);
return;
}
// URL looks like
// Shelly1: http events, URL looks like
// <ip address>:<remote port>/shelly/event/shellyrelay-XXXXXX/relay/n?xxxxx or
// <ip address>:<remote port>/shelly/event/shellyrelay-XXXXXX/roller/n?xxxxx or
// <ip address>:<remote port>/shelly/event/shellyht-XXXXXX/sensordata?hum=53,temp=26.50
String deviceName = "";
String index = "";
String type = "";
try {
String ipAddress = request.getRemoteAddr();
Map<String, String[]> parameters = request.getParameterMap();
logger.debug("ShellyEventServlet: {} Request from {}:{}{}?{}", request.getProtocol(), ipAddress,
request.getRemotePort(), path, parameters.toString());
if (!path.toLowerCase().startsWith(SHELLY1_CALLBACK_URI) || !path.contains("/event/shelly")) {
logger.warn("ShellyEventServlet received unknown request: path = {}", path);
return;
}
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 @@ public class ShellyEventServlet extends HttpServlet {
type = substringAfterLast(path, "/").toLowerCase();
}
logger.trace("{}: Process event of type type={}, index={}", deviceName, type, index);
Map<String, String> parms = new TreeMap<>();
Map<String, String> parms = new TreeMap<>();
for (Map.Entry<String, String[]> p : parameters.entrySet()) {
parms.put(p.getKey(), p.getValue()[0]);
@ -129,4 +126,39 @@ public class ShellyEventServlet extends HttpServlet {
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);
}
}
}

View File

@ -32,6 +32,7 @@ import org.eclipse.jetty.http.HttpFields;
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 @@ public class ShellyHttpClient {
}
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 @@ public class ShellyHttpClient {
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 @@ public class ShellyHttpClient {
}
} 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;

View File

@ -283,6 +283,7 @@ public class Shelly1ApiJsonDTO {
public Boolean enabled;
public String ssid;
public String key;
public Boolean rangeExtender; // Gen2 only
}
public static class ShellySettingsWiFiNetwork {
@ -605,6 +606,7 @@ public class Shelly1ApiJsonDTO {
@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 class Shelly1ApiJsonDTO {
@SerializedName("sleep_time") // Shelly Motion
public Integer sleepTime;
// Gen2
public Boolean ethernet;
public Boolean bluetooth;
}
public static class ShellySettingsAttributes {
@ -701,7 +707,8 @@ public class Shelly1ApiJsonDTO {
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 class Shelly1ApiJsonDTO {
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<ShellySettingsRelay> relays;
public ArrayList<ShellyRollerStatus> rollers;
public ArrayList<ShellyShortLightStatus> dimmers;
public Double voltage; // Shelly 2.5
public Integer input; // RGBW2 has no JSON array
public ArrayList<ShellyInputState> inputs;
public ArrayList<ShellyShortLightStatus> dimmers;
public ArrayList<ShellyRollerStatus> rollers;
public ArrayList<ShellySettingsLight> lights;
public ArrayList<ShellySettingsMeter> meters;
public ArrayList<ShellySettingsEMeter> emeters;
@SerializedName("ext_temperature")
@ -743,7 +751,6 @@ public class Shelly1ApiJsonDTO {
public ArrayList<ShellyThermnostat> thermostats;
public ShellySettingsUpdate update = new ShellySettingsUpdate();
@SerializedName("ram_total")
public Long ramTotal;
@SerializedName("ram_free")
@ -798,7 +805,6 @@ public class Shelly1ApiJsonDTO {
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 class Shelly1ApiJsonDTO {
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 class Shelly1ApiJsonDTO {
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
}

View File

@ -152,8 +152,7 @@ public class Shelly1CoIoTVersion1 extends Shelly1CoIoTProtocol implements Shelly
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

View File

@ -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<String, CoIotDescrBlk> blkMap = new LinkedHashMap<>();
@ -164,7 +162,7 @@ public class Shelly1CoapHandler implements Shelly1CoapListener {
@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 class Shelly1CoapHandler implements Shelly1CoapListener {
// error handling
logger.debug("{}: Unknown Response Code {} received, payload={}", thingName, code,
response.getPayloadString());
coiotErrors++;
thingHandler.incProtErrors();
return;
}
@ -205,14 +203,14 @@ public class Shelly1CoapHandler implements Shelly1CoapListener {
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 class Shelly1CoapHandler implements Shelly1CoapListener {
}
} catch (ShellyApiException e) {
logger.debug("{}: Unable to process CoIoT message: {}", thingName, e.toString());
coiotErrors++;
thingHandler.incProtErrors();
}
if (!updatesRequested) {
@ -296,7 +294,7 @@ public class Shelly1CoapHandler implements Shelly1CoapListener {
} 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 @@ public class Shelly1CoapHandler implements Shelly1CoapListener {
// 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 class Shelly1CoapHandler implements Shelly1CoapListener {
coiotBound = false;
}
public long getMessageCount() {
return coiotMessages;
}
public long getErrorCount() {
return coiotErrors;
}
public void dispose() {
stop();
}

View File

@ -151,6 +151,7 @@ public class Shelly1HttpApi extends ShellyHttpClient implements ShellyApiInterfa
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 class Shelly1HttpApi extends ShellyHttpClient implements ShellyApiInterfa
// 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 class Shelly1HttpApi extends ShellyHttpClient implements ShellyApiInterfa
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 @@ public class Shelly1HttpApi extends ShellyHttpClient implements ShellyApiInterfa
// 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 @@ public class Shelly1HttpApi extends ShellyHttpClient implements ShellyApiInterfa
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 class Shelly1HttpApi extends ShellyHttpClient implements ShellyApiInterfa
public int getTimeoutsRecovered() {
return timeoutsRecovered;
}
@Override
public void close() {
}
}

View File

@ -0,0 +1,548 @@
/**
* 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<ShellyRollerStatus> 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<String, String> 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<String, String> 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<String, String> 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<String, String> 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<String, String> 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<ShellySettingsInput> fillInputSettings(ShellyDeviceProfile profile,
Shelly2GetConfigResult dc) {
if (dc.input0 == null) {
return null; // device has no input
}
ArrayList<ShellySettingsInput> 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<ShellySettingsInput> 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());
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<String, String> 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!");
}
}

View File

@ -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<String> 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<Shelly2NotifyEvent> events;
}
public static class Shelly2RpcNotifyEvent {
public Double ts;
Shelly2NotifyEventData params;
}
}

View File

@ -0,0 +1,931 @@
/**
* 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) {
thing = thingTable.getThing(deviceIp);
logger.debug("{}: Get thing from thingTable", thingName);
}
}
@Override
public void onNotifyStatus(Shelly2RpcNotifyStatus message) {
logger.debug("{}: NotifyStatus update received: {}", thingName, gson.toJson(message));
try {
ShellyThingInterface t = thing;
if (t == null) {
logger.debug("{}: No matching thing on NotifyStatus for {}, ignore (src={}, dst={}, discovery={})",
thingName, thingName, message.src, message.dst, discovery);
return;
}
if (!t.isThingOnline() && t.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<Object> values = new ArrayList<>();
String boot = getString(ds.sys.wakeUpReason.boot);
String cause = getString(ds.sys.wakeUpReason.cause);
// Index 0 is aggregated status, 1 boot, 2 cause
String reason = boot.equals(SHELLY2_WAKEUPO_BOOT_RESTART) ? ALARM_TYPE_RESTARTED : cause;
values.add(reason);
values.add(ds.sys.wakeUpReason.boot);
values.add(ds.sys.wakeUpReason.cause);
getThing().updateWakeupReason(values);
}
fillDeviceStatus(status, ds, false);
return status;
}
@Override
public void setSleepTime(int value) throws ShellyApiException {
}
@Override
public ShellyStatusRelay getRelayStatus(int relayIndex) throws ShellyApiException {
if (getProfile().status.wifiSta.ssid == null) {
// Update status when not yet initialized
getStatus();
}
return relayStatus;
}
@Override
public void setRelayTurn(int id, String turnMode) throws ShellyApiException {
Shelly2RpcRequestParams params = new Shelly2RpcRequestParams();
params.id = id;
params.on = SHELLY_API_ON.equals(turnMode);
apiRequest(SHELLYRPC_METHOD_SWITCH_SET, params, String.class);
}
@Override
public ShellyRollerStatus getRollerStatus(int rollerIndex) throws ShellyApiException {
if (rollerIndex < rollerStatus.size()) {
return rollerStatus.get(rollerIndex);
}
throw new IllegalArgumentException("Invalid rollerIndex on getRollerStatus");
}
@Override
public void setRollerTurn(int relayIndex, String turnMode) throws ShellyApiException {
String operation = "";
switch (turnMode) {
case SHELLY_ALWD_ROLLER_TURN_OPEN:
operation = SHELLY2_COVER_CMD_OPEN;
break;
case SHELLY_ALWD_ROLLER_TURN_CLOSE:
operation = SHELLY2_COVER_CMD_CLOSE;
break;
case SHELLY_ALWD_ROLLER_TURN_STOP:
operation = SHELLY2_COVER_CMD_STOP;
break;
}
apiRequest(new Shelly2RpcRequest().withMethod("Cover." + operation).withId(relayIndex));
}
@Override
public void setRollerPos(int relayIndex, int position) throws ShellyApiException {
apiRequest(
new Shelly2RpcRequest().withMethod(SHELLYRPC_METHOD_COVER_SETPOS).withId(relayIndex).withPos(position));
}
@Override
public ShellyStatusSensor getSensorStatus() throws ShellyApiException {
return sensorData;
}
@Override
public void setAutoTimer(int index, String timerName, double value) throws ShellyApiException {
Shelly2RpcRequest req = new Shelly2RpcRequest().withMethod(SHELLYRPC_METHOD_SWITCH_SETCONFIG).withId(index);
req.params.withConfig();
req.params.config.name = "Switch" + index;
if (timerName.equals(SHELLY_TIMER_AUTOON)) {
req.params.config.autoOn = value > 0;
req.params.config.autoOnDelay = value;
} else {
req.params.config.autoOff = value > 0;
req.params.config.autoOffDelay = value;
}
apiRequest(req);
}
@Override
public ShellySettingsLogin getLoginSettings() throws ShellyApiException {
return new ShellySettingsLogin();
}
@Override
public ShellySettingsLogin setLoginCredentials(String user, String password) throws ShellyApiException {
Shelly2RpcRequestParams params = new Shelly2RpcRequestParams();
params.user = "admin";
params.realm = config.serviceName;
params.ha1 = sha256(params.user + ":" + params.realm + ":" + password);
apiRequest(SHELLYRPC_METHOD_AUTHSET, params, String.class);
ShellySettingsLogin res = new ShellySettingsLogin();
res.enabled = true;
res.username = params.user;
res.password = password;
return new ShellySettingsLogin();
}
@Override
public boolean setWiFiRangeExtender(boolean enable) throws ShellyApiException {
Shelly2RpcRequestParams params = new Shelly2RpcRequestParams().withConfig();
params.config.ap = new Shelly2DeviceConfigAp();
params.config.ap.rangeExtender = new Shelly2DeviceConfigApRE();
params.config.ap.rangeExtender.enable = enable;
Shelly2WsConfigResult res = apiRequest(SHELLYRPC_METHOD_WIFISETCONG, params, Shelly2WsConfigResult.class);
return res.restartRequired;
}
@Override
public boolean setEthernet(boolean enable) throws ShellyApiException {
Shelly2RpcRequestParams params = new Shelly2RpcRequestParams().withConfig();
params.config.enable = enable;
Shelly2WsConfigResult res = apiRequest(SHELLYRPC_METHOD_ETHSETCONG, params, Shelly2WsConfigResult.class);
return res.restartRequired;
}
@Override
public boolean setBluetooth(boolean enable) throws ShellyApiException {
Shelly2RpcRequestParams params = new Shelly2RpcRequestParams().withConfig();
params.config.enable = enable;
Shelly2WsConfigResult res = apiRequest(SHELLYRPC_METHOD_BLESETCONG, params, Shelly2WsConfigResult.class);
return res.restartRequired;
}
@Override
public String deviceReboot() throws ShellyApiException {
return apiRequest(SHELLYRPC_METHOD_REBOOT, null, String.class);
}
@Override
public String factoryReset() throws ShellyApiException {
return apiRequest(SHELLYRPC_METHOD_RESET, null, String.class);
}
@Override
public ShellyOtaCheckResult checkForUpdate() throws ShellyApiException {
Shelly2DeviceStatusSysAvlUpdate status = apiRequest(SHELLYRPC_METHOD_CHECKUPD, null,
Shelly2DeviceStatusSysAvlUpdate.class);
ShellyOtaCheckResult result = new ShellyOtaCheckResult();
result.status = status.stable != null || status.beta != null ? "new" : "ok";
return result;
}
@Override
public ShellySettingsUpdate firmwareUpdate(String fwurl) throws ShellyApiException {
ShellySettingsUpdate res = new ShellySettingsUpdate();
boolean prod = fwurl.contains("update");
boolean beta = fwurl.contains("beta");
Shelly2RpcRequestParams params = new Shelly2RpcRequestParams();
if (prod || beta) {
params.stage = prod || beta ? "stable" : "beta";
} else {
params.url = fwurl;
}
apiRequest(SHELLYRPC_METHOD_UPDATE, params, String.class);
res.status = "Update initiated";
return res;
}
@Override
public String setCloud(boolean enable) throws ShellyApiException {
Shelly2RpcRequestParams params = new Shelly2RpcRequestParams().withConfig();
params.config.enable = enable;
Shelly2WsConfigResult res = apiRequest(SHELLYRPC_METHOD_CLOUDSET, params, Shelly2WsConfigResult.class);
return res.restartRequired ? "restart required" : "ok";
}
@Override
public String setDebug(boolean enabled) throws ShellyApiException {
return "failed";
}
@Override
public String getDebugLog(String id) throws ShellyApiException {
return ""; // Gen2 uses WS to publish debug log
}
/*
* The following API calls are not yet relevant, because currently there a no Plus/Pro (Gen2) devices of those
* categories (e.g. bulbs)
*/
@Override
public void setLedStatus(String ledName, Boolean value) throws ShellyApiException {
throw new ShellyApiException("API call not implemented");
}
@Override
public ShellyStatusLight getLightStatus() throws ShellyApiException {
throw new ShellyApiException("API call not implemented");
}
@Override
public ShellyShortLightStatus getLightStatus(int index) throws ShellyApiException {
throw new ShellyApiException("API call not implemented");
}
@Override
public void setLightParm(int lightIndex, String parm, String value) throws ShellyApiException {
throw new ShellyApiException("API call not implemented");
}
@Override
public void setLightParms(int lightIndex, Map<String, String> parameters) throws ShellyApiException {
throw new ShellyApiException("API call not implemented");
}
@Override
public ShellyShortLightStatus setLightTurn(int id, String turnMode) throws ShellyApiException {
throw new ShellyApiException("API call not implemented");
}
@Override
public void setBrightness(int id, int brightness, boolean autoOn) throws ShellyApiException {
throw new ShellyApiException("API call not implemented");
}
@Override
public void setLightMode(String mode) throws ShellyApiException {
throw new ShellyApiException("API call not implemented");
}
@Override
public void setValveMode(int valveId, boolean auto) throws ShellyApiException {
throw new ShellyApiException("API call not implemented");
}
@Override
public void setValvePosition(int valveId, double value) throws ShellyApiException {
throw new ShellyApiException("API call not implemented");
}
@Override
public void setValveTemperature(int valveId, int value) throws ShellyApiException {
throw new ShellyApiException("API call not implemented");
}
@Override
public void setValveProfile(int valveId, int value) throws ShellyApiException {
throw new ShellyApiException("API call not implemented");
}
@Override
public void setValveBoostTime(int valveId, int value) throws ShellyApiException {
throw new ShellyApiException("API call not implemented");
}
@Override
public void startValveBoost(int valveId, int value) throws ShellyApiException {
throw new ShellyApiException("API call not implemented");
}
@Override
public String resetStaCache() throws ShellyApiException {
throw new ShellyApiException("API call not implemented");
}
@Override
public void setActionURLs() throws ShellyApiException {
// not relevant for Gen2
}
@Override
public ShellySettingsLogin setCoIoTPeer(String peer) throws ShellyApiException {
// not relevant for Gen2
return new ShellySettingsLogin();
}
@Override
public String getCoIoTDescription() {
return ""; // not relevant to Gen2
}
@Override
public void sendIRKey(String keyCode) throws ShellyApiException, IllegalArgumentException {
throw new ShellyApiException("API call not implemented");
}
@Override
public String setWiFiRecovery(boolean enable) throws ShellyApiException {
return "failed"; // not supported by Gen2
}
@Override
public String setApRoaming(boolean enable) throws ShellyApiException {
return "false";// not supported by Gen2
}
private void asyncApiRequest(String method) throws ShellyApiException {
Shelly2RpcBaseMessage request = buildRequest(method, null);
reconnect();
rpcSocket.sendMessage(gson.toJson(request)); // submit, result wull be async
}
public <T> T apiRequest(String method, @Nullable Object params, Class<T> classOfT) throws ShellyApiException {
String json = "";
Shelly2RpcBaseMessage req = buildRequest(method, params);
try {
reconnect(); // make sure WS is connected
if (authInfo.realm != null) {
req.auth = buildAuthRequest(authInfo, config.userId, config.serviceName, config.password);
}
json = rpcPost(gson.toJson(req));
} catch (ShellyApiException e) {
ShellyApiResult res = e.getApiResult();
String auth = getString(res.authResponse);
if (res.isHttpAccessUnauthorized() && !auth.isEmpty()) {
String[] options = auth.split(",");
for (String o : options) {
String key = substringBefore(o, "=").stripLeading().trim();
String value = substringAfter(o, "=").replaceAll("\"", "").trim();
switch (key) {
case "Digest qop":
break;
case "realm":
authInfo.realm = value;
break;
case "nonce":
authInfo.nonce = Long.parseLong(value, 16);
break;
case "algorithm":
authInfo.algorithm = value;
break;
}
}
authInfo.nc = 1;
req.auth = buildAuthRequest(authInfo, config.userId, authInfo.realm, config.password);
json = rpcPost(gson.toJson(req));
} else {
throw e;
}
}
json = gson.toJson(gson.fromJson(json, Shelly2RpcBaseMessage.class).result);
return fromJson(gson, json, classOfT);
}
public <T> T apiRequest(Shelly2RpcRequest request, Class<T> classOfT) throws ShellyApiException {
return apiRequest(request.method, request.params, classOfT);
}
public String apiRequest(Shelly2RpcRequest request) throws ShellyApiException {
return apiRequest(request.method, request.params, String.class);
}
private String rpcPost(String postData) throws ShellyApiException {
return httpPost("/rpc", postData);
}
private void reconnect() throws ShellyApiException {
if (!rpcSocket.isConnected()) {
logger.debug("{}: Connect Rpc Socket (discovery = {})", thingName, discovery);
rpcSocket.connect();
}
}
private void disconnect() {
if (rpcSocket.isConnected()) {
rpcSocket.disconnect();
}
}
public Shelly2RpctInterface getRpcHandler() {
return this;
}
@Override
public void close() {
logger.debug("{}: Closing Rpc API (socket is {}, discovery={})", thingName,
rpcSocket.isConnected() ? "connected" : "disconnected", discovery);
disconnect();
initialized = false;
}
private void incProtErrors() {
if (thing != null) {
thing.incProtErrors();
}
}
}

View File

@ -0,0 +1,311 @@
/**
* 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.api2.Shelly2ApiJsonDTO.*;
import static org.openhab.binding.shelly.internal.util.ShellyUtils.*;
import java.io.IOException;
import java.net.URI;
import java.util.concurrent.CountDownLatch;
import javax.ws.rs.core.HttpHeaders;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.StatusCode;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketError;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage;
import org.eclipse.jetty.websocket.api.annotations.WebSocket;
import org.eclipse.jetty.websocket.client.ClientUpgradeRequest;
import org.eclipse.jetty.websocket.client.WebSocketClient;
import org.openhab.binding.shelly.internal.api.ShellyApiException;
import org.openhab.binding.shelly.internal.api1.Shelly1HttpApi;
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.handler.ShellyThingInterface;
import org.openhab.binding.shelly.internal.handler.ShellyThingTable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
/**
* {@link Shelly1HttpApi} wraps the Shelly REST API and provides various low level function to access the device api
* (not
* cloud api).
*
* @author Markus Michels - Initial contribution
*/
@NonNullByDefault
@WebSocket(maxIdleTime = Integer.MAX_VALUE)
public class Shelly2RpcSocket {
private final Logger logger = LoggerFactory.getLogger(Shelly2RpcSocket.class);
private final Gson gson = new Gson();
private String thingName = "";
private String deviceIp = "";
private boolean inbound = false;
private CountDownLatch connectLatch = new CountDownLatch(1);
private @Nullable Session session;
private @Nullable Shelly2RpctInterface websocketHandler;
private WebSocketClient client = new WebSocketClient();
private @Nullable ShellyThingTable thingTable;
public Shelly2RpcSocket() {
}
/**
* Regular constructor for Thing and Discover handler
*
* @param thingName Thing/Service name
* @param thingTable
* @param deviceIp IP address for the device
*/
public Shelly2RpcSocket(String thingName, @Nullable ShellyThingTable thingTable, String deviceIp) {
this.thingName = thingName;
this.deviceIp = deviceIp;
this.thingTable = thingTable;
}
/**
* Constructor called from Servlet handler
*
* @param thingTable
* @param inbound
*/
public Shelly2RpcSocket(ShellyThingTable thingTable, boolean inbound) {
this.thingTable = thingTable;
this.inbound = inbound;
}
/**
* Add listener for inbound messages implementing Shelly2RpctInterface
*
* @param interfacehandler
*/
public void addMessageHandler(Shelly2RpctInterface interfacehandler) {
this.websocketHandler = interfacehandler;
}
/**
* Connect outbound Web Socket
*
* @throws ShellyApiException
*/
public void connect() throws ShellyApiException {
try {
disconnect(); // for safety
URI uri = new URI("ws://" + deviceIp + "/rpc");
ClientUpgradeRequest request = new ClientUpgradeRequest();
request.setHeader(HttpHeaders.HOST, deviceIp);
request.setHeader("Origin", "http://" + deviceIp);
request.setHeader("Pragma", "no-cache");
request.setHeader("Cache-Control", "no-cache");
logger.debug("{}: Connect WebSocket, URI={}", thingName, uri);
client = new WebSocketClient();
connectLatch = new CountDownLatch(1);
client.start();
client.setConnectTimeout(5000);
client.setStopTimeout(0);
client.connect(this, uri, request);
} catch (Exception e) {
throw new ShellyApiException("Unable to initialize WebSocket", e);
}
}
/**
* Web Socket is connected, lookup thing and create connectLatch to synchronize first sendMessage()
*
* @param session Newly created WebSocket connection
*/
@OnWebSocketConnect
public void onConnect(Session session) {
try {
if (session.getRemoteAddress() == null) {
logger.debug("{}: Invalid inbound WebSocket connect", thingName);
session.close(StatusCode.ABNORMAL, "Invalid remote IP");
return;
}
this.session = session;
if (deviceIp.isEmpty()) {
// This is the inbound event web socket
deviceIp = session.getRemoteAddress().getAddress().getHostAddress();
}
if (websocketHandler == null) {
if (thingTable != null) {
ShellyThingInterface thing = thingTable.getThing(deviceIp);
Shelly2ApiRpc api = (Shelly2ApiRpc) thing.getApi();
websocketHandler = api.getRpcHandler();
}
}
connectLatch.countDown();
logger.debug("{}: WebSocket connected {}<-{}, Idle Timeout={}", thingName, session.getLocalAddress(),
session.getRemoteAddress(), session.getIdleTimeout());
if (websocketHandler != null) {
websocketHandler.onConnect(deviceIp, true);
return;
}
} catch (IllegalArgumentException e) { // unknown thing
// debug is below
}
if (websocketHandler == null && thingTable != null) {
logger.debug("Rpc: Unable to handle connection from {} (unknown/disabled thing), closing socket", deviceIp);
session.close(StatusCode.SHUTDOWN, "Thing not active");
}
}
/**
* Send request over WebSocket
*
* @param str API request message
* @throws ShellyApiException
*/
@SuppressWarnings("null")
public void sendMessage(String str) throws ShellyApiException {
if (session != null) {
try {
connectLatch.await();
session.getRemote().sendString(str);
return;
} catch (IOException | InterruptedException e) {
throw new ShellyApiException("Error RpcSend failed", e);
}
}
throw new ShellyApiException("Unable to send API request (No Rpc session)");
}
/**
* Close WebSocket session
*/
public void disconnect() {
try {
if (session != null) {
Session s = session;
if (s.isOpen()) {
logger.debug("{}: Disconnecting WebSocket ({} -> {})", thingName, s.getLocalAddress(),
s.getRemoteAddress());
s.disconnect();
}
s.close(StatusCode.NORMAL, "Socket closed");
session = null;
}
if (client.isStarted()) {
client.stop();
}
} catch (Exception e) {
if (e.getCause() instanceof InterruptedException) {
logger.debug("{}: Unable to close socket - interrupted", thingName); // e.g. device was rebooted
} else {
logger.debug("{}: Unable to close socket", thingName, e);
}
}
}
/**
* Inbound WebSocket message
*
* @param session WebSpcket session
* @param receivedMessage Textial API message
*/
@OnWebSocketMessage
public void onText(Session session, String receivedMessage) {
try {
Shelly2RpctInterface handler = websocketHandler;
Shelly2RpcBaseMessage message = fromJson(gson, receivedMessage, Shelly2RpcBaseMessage.class);
logger.trace("{}: Inbound Rpc message: {}", thingName, receivedMessage);
if (handler != null) {
if (thingName.isEmpty()) {
thingName = getString(message.src);
}
if (message.method == null) {
message.method = SHELLYRPC_METHOD_NOTIFYFULLSTATUS;
}
switch (getString(message.method)) {
case SHELLYRPC_METHOD_NOTIFYSTATUS:
case SHELLYRPC_METHOD_NOTIFYFULLSTATUS:
Shelly2RpcNotifyStatus status = fromJson(gson, receivedMessage, Shelly2RpcNotifyStatus.class);
if (status.params == null) {
status.params = status.result;
}
handler.onNotifyStatus(status);
return;
case SHELLYRPC_METHOD_NOTIFYEVENT:
handler.onNotifyEvent(fromJson(gson, receivedMessage, Shelly2RpcNotifyEvent.class));
return;
default:
handler.onMessage(receivedMessage);
}
} else {
logger.debug("{}: No Rpc listener registered for device {}, skip message: {}", thingName,
getString(message.src), receivedMessage);
}
} catch (ShellyApiException | IllegalArgumentException | NullPointerException e) {
logger.debug("{}: Unable to process Rpc message: {}", thingName, receivedMessage, e);
}
}
public boolean isConnected() {
return session != null && session.isOpen();
}
public boolean isInbound() {
return inbound;
}
/**
* Web Socket closed, notify thing handler
*
* @param statusCode StatusCode
* @param reason Textual reason
*/
@OnWebSocketClose
public void onClose(int statusCode, String reason) {
if (statusCode != StatusCode.NORMAL) {
logger.trace("{}: Rpc connection closed: {} - {}", thingName, statusCode, getString(reason));
}
if (inbound) {
// Ignore disconnect: Device establishes the socket, sends NotifyxFullStatus and disconnects
return;
}
disconnect();
if (websocketHandler != null) {
websocketHandler.onClose(statusCode, reason);
}
}
/**
* WebSocket error handler
*
* @param cause WebSocket error/Exception
*/
@OnWebSocketError
public void onError(Throwable cause) {
if (inbound) {
// Ignore disconnect: Device establishes the socket, sends NotifyxFullStatus and disconnects
return;
}
if (websocketHandler != null) {
websocketHandler.onError(cause);
}
}
}

View File

@ -0,0 +1,38 @@
/**
* 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 org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2RpcNotifyEvent;
import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2RpcNotifyStatus;
/**
* The {@link WebsocketInterface} is responsible for interfacing the Websocket.
*
* @author Markus Michels - Initial contribution
*/
@NonNullByDefault
public interface Shelly2RpctInterface {
public void onConnect(String deviceIp, boolean connected);
public void onMessage(String decodedmessage);
public void onNotifyStatus(Shelly2RpcNotifyStatus message);
public void onNotifyEvent(Shelly2RpcNotifyEvent message);
public void onClose(int statusCode, String reason);
public void onError(Throwable cause);
}

View File

@ -27,9 +27,11 @@ 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.ShellyApiInterface;
import org.openhab.binding.shelly.internal.api.ShellyApiResult;
import org.openhab.binding.shelly.internal.api.ShellyDeviceProfile;
import org.openhab.binding.shelly.internal.api1.Shelly1HttpApi;
import org.openhab.binding.shelly.internal.api2.Shelly2ApiRpc;
import org.openhab.binding.shelly.internal.config.ShellyBindingConfiguration;
import org.openhab.binding.shelly.internal.config.ShellyThingConfiguration;
import org.openhab.binding.shelly.internal.handler.ShellyBaseHandler;
@ -139,15 +141,20 @@ public class ShellyDiscoveryParticipant implements MDNSDiscoveryParticipant {
config.userId = bindingConfig.defaultUserId;
config.password = bindingConfig.defaultPassword;
boolean gen2 = "2".equals(service.getPropertyString("gen"));
try {
Shelly1HttpApi api = new Shelly1HttpApi(name, config, httpClient);
ShellyApiInterface api = gen2 ? new Shelly2ApiRpc(name, config, httpClient)
: new Shelly1HttpApi(name, config, httpClient);
if (name.contains("plushat")) {
int i = 1;
}
api.initialize();
profile = api.getDeviceProfile(thingType);
api.close();
logger.debug("{}: Shelly settings : {}", name, profile.settingsJson);
deviceName = profile.name;
model = profile.deviceType;
mode = profile.mode;
properties = ShellyBaseHandler.fillDeviceProperties(profile);
logger.trace("{}: thingType={}, deviceType={}, mode={}, symbolic name={}", name, thingType,
profile.deviceType, mode.isEmpty() ? "<standard>" : mode, deviceName);
@ -174,7 +181,7 @@ public class ShellyDiscoveryParticipant implements MDNSDiscoveryParticipant {
addProperty(properties, PROPERTY_SERVICE_NAME, name);
addProperty(properties, PROPERTY_DEV_NAME, deviceName);
addProperty(properties, PROPERTY_DEV_TYPE, thingType);
addProperty(properties, PROPERTY_DEV_GEN, "1");
addProperty(properties, PROPERTY_DEV_GEN, gen2 ? "2" : "1");
addProperty(properties, PROPERTY_DEV_MODE, mode);
logger.debug("{}: Adding Shelly {}, UID={}", name, deviceName, thingUID.getAsString());

View File

@ -69,6 +69,8 @@ public class ShellyThingCreator {
public static final String SHELLYDT_PLUS1PMUL = "SNSW-001P15UL";
public static final String SHELLYDT_PLUS2PM_RELAY = "SNSW-002P16EU-relay";
public static final String SHELLYDT_PLUS2PM_ROLLER = "SNSW-002P16EU-roller";
public static final String SHELLYDT_PLUS2PM_RELAY_2 = "SNSW-102P16EU-relay";
public static final String SHELLYDT_PLUS2PM_ROLLER_2 = "SNSW-102P16EU-roller";
public static final String SHELLYDT_PLUSPLUGUS = "SNPL-00116US";
public static final String SHELLYDT_PLUSI4 = "SNSN-0024X";
public static final String SHELLYDT_PLUSI4DC = "SNSN-0D24X";
@ -77,17 +79,19 @@ public class ShellyThingCreator {
// Shelly Pro Series
public static final String SHELLYDT_PRO1 = "SPSW-001XE16EU";
public static final String SHELLYDT_PRO1_2 = "SPSW-101XE16EU";
public static final String SHELLYDT_PRO1_3 = "SPSW-201XE16EU";
public static final String SHELLYDT_PRO1PM = "SPSW-001PE16EU";
public static final String SHELLYDT_PRO1PM_ = "SPSW-201PE16EU";
public static final String SHELLYDT_PRO1PM_2 = "SPSW-101PE16EU";
public static final String SHELLYDT_PRO1PM_3 = "SPSW-201PE16EU";
public static final String SHELLYDT_PRO2_RELAY = "SPSW-002XE16EU-relay";
public static final String SHELLYDT_PRO2_ROLLER = "SPSW-002XE16EU-roller";
public static final String SHELLYDT_PRO2_RELAY_2 = "SPSW-102XE16EU-relay";
public static final String SHELLYDT_PRO2_ROLLER_2 = "SPSW-102XE16EU-roller";
public static final String SHELLYDT_PRO2_RELAY_3 = "SPSW-202XE16EU-relay";
public static final String SHELLYDT_PRO2PM_RELAY = "SPSW-002PE16EU-relay";
public static final String SHELLYDT_PRO2PM_ROLLER = "SPSW-002PE16EU-roller";
public static final String SHELLYDT_PRO2PM_RELAY_2 = "SPSW-002PE16EU-relay";
public static final String SHELLYDT_PRO2PM_ROLLER_2 = "SPSW-002PE16EU-roller";
public static final String SHELLYDT_PRO2PM_RELAY_2 = "SPSW-102PE16EU-relay";
public static final String SHELLYDT_PRO2PM_ROLLER_2 = "SPSW-102PE16EU-roller";
public static final String SHELLYDT_PRO2PM_RELAY_3 = "SPSW-202PE16EU-relay";
public static final String SHELLYDT_PRO2PM_ROLLER_3 = "SPSW-202PE16EU-roller";
public static final String SHELLYDT_PRO3 = "SPSW-003XE16EU";
public static final String SHELLYDT_PRO4PM = "SPSW-004PE16EU";
public static final String SHELLYDT_PRO4PM_2 = "SPSW-104PE16EU";
@ -100,7 +104,6 @@ public class ShellyThingCreator {
public static final String THING_TYPE_SHELLY3EM_STR = "shellyem3"; // bad: misspelled product name, it's 3EM
public static final String THING_TYPE_SHELLY2_PREFIX = "shellyswitch";
public static final String THING_TYPE_SHELLY2_RELAY_STR = "shelly2-relay";
public static final String THING_TYPE_SHELLY2_ROLLER_STR = "shelly2-roller";
public static final String THING_TYPE_SHELLY25_PREFIX = "shellyswitch25";
public static final String THING_TYPE_SHELLY25_RELAY_STR = "shelly25-relay";
public static final String THING_TYPE_SHELLY25_ROLLER_STR = "shelly25-roller";
@ -147,7 +150,6 @@ public class ShellyThingCreator {
public static final String THING_TYPE_SHELLYPRO1_STR = "shellypro1";
public static final String THING_TYPE_SHELLYPRO1PM_STR = "shellypro1pm";
public static final String THING_TYPE_SHELLYPRO2_RELAY_STR = "shellypro2-relay";
public static final String THING_TYPE_SHELLYPRO2_ROLLER_STR = "shellypro2-roller";
public static final String THING_TYPE_SHELLYPRO2PM_RELAY_STR = "shellypro2pm-relay";
public static final String THING_TYPE_SHELLYPRO2PM_ROLLER_STR = "shellypro2pm-roller";
public static final String THING_TYPE_SHELLYPRO3_STR = "shellypro3";
@ -164,8 +166,6 @@ public class ShellyThingCreator {
public static final ThingTypeUID THING_TYPE_SHELLY3EM = new ThingTypeUID(BINDING_ID, THING_TYPE_SHELLY3EM_STR);
public static final ThingTypeUID THING_TYPE_SHELLY2_RELAY = new ThingTypeUID(BINDING_ID,
THING_TYPE_SHELLY2_RELAY_STR);
public static final ThingTypeUID THING_TYPE_SHELLY2_ROLLER = new ThingTypeUID(BINDING_ID,
THING_TYPE_SHELLY2_ROLLER_STR);
public static final ThingTypeUID THING_TYPE_SHELLY25_RELAY = new ThingTypeUID(BINDING_ID,
THING_TYPE_SHELLY25_RELAY_STR);
public static final ThingTypeUID THING_TYPE_SHELLY25_ROLLER = new ThingTypeUID(BINDING_ID,
@ -233,8 +233,6 @@ public class ShellyThingCreator {
THING_TYPE_SHELLYPRO1PM_STR);
public static final ThingTypeUID THING_TYPE_SHELLYPRO2_RELAY = new ThingTypeUID(BINDING_ID,
THING_TYPE_SHELLYPRO2_RELAY_STR);
public static final ThingTypeUID THING_TYPE_SHELLYPRO2_ROLLER = new ThingTypeUID(BINDING_ID,
THING_TYPE_SHELLYPRO2_ROLLER_STR);
public static final ThingTypeUID THING_TYPE_SHELLYPRO2PM_RELAY = new ThingTypeUID(BINDING_ID,
THING_TYPE_SHELLYPRO2PM_RELAY_STR);
public static final ThingTypeUID THING_TYPE_SHELLYPRO2PM_ROLLER = new ThingTypeUID(BINDING_ID,
@ -277,6 +275,8 @@ public class ShellyThingCreator {
THING_TYPE_MAPPING.put(SHELLYDT_PLUS1PMUL, THING_TYPE_SHELLYPLUS1PM_STR);
THING_TYPE_MAPPING.put(SHELLYDT_PLUS2PM_RELAY, THING_TYPE_SHELLYPLUS2PM_RELAY_STR);
THING_TYPE_MAPPING.put(SHELLYDT_PLUS2PM_ROLLER, THING_TYPE_SHELLYPLUS2PM_ROLLER_STR);
THING_TYPE_MAPPING.put(SHELLYDT_PLUS2PM_RELAY_2, THING_TYPE_SHELLYPLUS2PM_RELAY_STR);
THING_TYPE_MAPPING.put(SHELLYDT_PLUS2PM_ROLLER_2, THING_TYPE_SHELLYPLUS2PM_ROLLER_STR);
THING_TYPE_MAPPING.put(SHELLYDT_PLUSPLUGUS, THING_TYPE_SHELLYPLUSPLUGUS_STR);
THING_TYPE_MAPPING.put(SHELLYDT_PLUSI4DC, THING_TYPE_SHELLYPLUSI4DC_STR);
THING_TYPE_MAPPING.put(SHELLYDT_PLUSI4, THING_TYPE_SHELLYPLUSI4_STR);
@ -285,16 +285,19 @@ public class ShellyThingCreator {
// Pro Series
THING_TYPE_MAPPING.put(SHELLYDT_PRO1, THING_TYPE_SHELLYPRO1_STR);
THING_TYPE_MAPPING.put(SHELLYDT_PRO1_2, THING_TYPE_SHELLYPRO1_STR);
THING_TYPE_MAPPING.put(SHELLYDT_PRO1_3, THING_TYPE_SHELLYPRO1_STR);
THING_TYPE_MAPPING.put(SHELLYDT_PRO1PM, THING_TYPE_SHELLYPRO1PM_STR);
THING_TYPE_MAPPING.put(SHELLYDT_PRO1PM_2, THING_TYPE_SHELLYPRO1PM_STR);
THING_TYPE_MAPPING.put(SHELLYDT_PRO1PM_3, THING_TYPE_SHELLYPRO1PM_STR);
THING_TYPE_MAPPING.put(SHELLYDT_PRO2_RELAY, THING_TYPE_SHELLYPRO2_RELAY_STR);
THING_TYPE_MAPPING.put(SHELLYDT_PRO2_RELAY_2, THING_TYPE_SHELLYPRO2_RELAY_STR);
THING_TYPE_MAPPING.put(SHELLYDT_PRO2_ROLLER, THING_TYPE_SHELLYPRO2_ROLLER_STR);
THING_TYPE_MAPPING.put(SHELLYDT_PRO2_ROLLER_2, THING_TYPE_SHELLYPRO2_ROLLER_STR);
THING_TYPE_MAPPING.put(SHELLYDT_PRO2_RELAY_3, THING_TYPE_SHELLYPRO2_RELAY_STR);
THING_TYPE_MAPPING.put(SHELLYDT_PRO2PM_RELAY, THING_TYPE_SHELLYPRO2PM_RELAY_STR);
THING_TYPE_MAPPING.put(SHELLYDT_PRO2PM_RELAY_2, THING_TYPE_SHELLYPRO2PM_RELAY_STR);
THING_TYPE_MAPPING.put(SHELLYDT_PRO2PM_RELAY_3, THING_TYPE_SHELLYPRO2PM_RELAY_STR);
THING_TYPE_MAPPING.put(SHELLYDT_PRO2PM_ROLLER, THING_TYPE_SHELLYPRO2PM_ROLLER_STR);
THING_TYPE_MAPPING.put(SHELLYDT_PRO2PM_ROLLER_2, THING_TYPE_SHELLYPRO2PM_ROLLER_STR);
THING_TYPE_MAPPING.put(SHELLYDT_PRO2PM_ROLLER_3, THING_TYPE_SHELLYPRO2PM_ROLLER_STR);
THING_TYPE_MAPPING.put(SHELLYDT_PRO3, THING_TYPE_SHELLYPRO3_STR);
THING_TYPE_MAPPING.put(SHELLYDT_PRO4PM, THING_TYPE_SHELLYPRO4PM_STR);
THING_TYPE_MAPPING.put(SHELLYDT_PRO4PM_2, THING_TYPE_SHELLYPRO4PM_STR);
@ -364,7 +367,7 @@ public class ShellyThingCreator {
return mode.equals(SHELLY_MODE_RELAY) ? THING_TYPE_SHELLY25_RELAY_STR : THING_TYPE_SHELLY25_ROLLER_STR;
}
if (name.startsWith(THING_TYPE_SHELLY2_PREFIX)) { // Shelly v2
return mode.equals(SHELLY_MODE_RELAY) ? THING_TYPE_SHELLY2_RELAY_STR : THING_TYPE_SHELLY2_ROLLER_STR;
return THING_TYPE_SHELLY2_RELAY_STR;
}
if (name.startsWith(THING_TYPE_SHELLYPLUG_STR)) {
// shellyplug-s needs to be mapped to shellyplugs to follow the schema

View File

@ -21,13 +21,9 @@ import static org.openhab.core.thing.Thing.*;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
@ -39,19 +35,21 @@ 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;
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.ShellyOtaCheckResult;
import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsDevice;
import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsStatus;
import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyThermnostat;
import org.openhab.binding.shelly.internal.api1.Shelly1CoapHandler;
import org.openhab.binding.shelly.internal.api1.Shelly1CoapJSonDTO;
import org.openhab.binding.shelly.internal.api1.Shelly1CoapServer;
import org.openhab.binding.shelly.internal.api1.Shelly1HttpApi;
import org.openhab.binding.shelly.internal.api2.Shelly2ApiRpc;
import org.openhab.binding.shelly.internal.config.ShellyBindingConfiguration;
import org.openhab.binding.shelly.internal.config.ShellyThingConfiguration;
import org.openhab.binding.shelly.internal.discovery.ShellyThingCreator;
import org.openhab.binding.shelly.internal.provider.ShellyChannelDefinitions;
import org.openhab.binding.shelly.internal.provider.ShellyStateDescriptionProvider;
import org.openhab.binding.shelly.internal.provider.ShellyTranslationProvider;
import org.openhab.binding.shelly.internal.util.ShellyChannelCache;
import org.openhab.binding.shelly.internal.util.ShellyVersionDTO;
@ -66,7 +64,6 @@ import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.binding.BaseThingHandler;
import org.openhab.core.thing.binding.ThingHandlerService;
import org.openhab.core.thing.binding.builder.ThingBuilder;
import org.openhab.core.thing.type.ChannelTypeUID;
import org.openhab.core.types.Command;
@ -84,23 +81,11 @@ import org.slf4j.LoggerFactory;
* @author Markus Michels - Initial contribution
*/
@NonNullByDefault
public class ShellyBaseHandler extends BaseThingHandler
implements ShellyDeviceListener, ShellyManagerInterface, ShellyThingInterface {
private class OptionEntry {
public ChannelTypeUID uid;
public String key;
public String value;
public OptionEntry(ChannelTypeUID uid, String key, String value) {
this.uid = uid;
this.key = key;
this.value = value;
}
}
public abstract class ShellyBaseHandler extends BaseThingHandler
implements ShellyThingInterface, ShellyDeviceListener, ShellyManagerInterface {
protected final Logger logger = LoggerFactory.getLogger(ShellyBaseHandler.class);
protected final ShellyChannelDefinitions channelDefinitions;
private final CopyOnWriteArrayList<OptionEntry> stateOptions = new CopyOnWriteArrayList<>();
public String thingName = "";
public String thingType = "";
@ -111,28 +96,29 @@ public class ShellyBaseHandler extends BaseThingHandler
private ShellyBindingConfiguration bindingConfig;
protected ShellyThingConfiguration config = new ShellyThingConfiguration();
protected ShellyDeviceProfile profile = new ShellyDeviceProfile(); // init empty profile to avoid NPE
protected ShellyDeviceStats stats = new ShellyDeviceStats();
private final Shelly1CoapHandler coap;
public boolean autoCoIoT = false;
private ShellyDeviceStats stats = new ShellyDeviceStats();
private @Nullable Shelly1CoapHandler coap;
private final ShellyTranslationProvider messages;
protected boolean stopping = false;
private final ShellyChannelCache cache;
private final int cacheCount = UPDATE_SETTINGS_INTERVAL_SECONDS / UPDATE_STATUS_INTERVAL_SECONDS;
private final boolean gen2;
protected boolean autoCoIoT = false;
// Thing status
private boolean channelsCreated = false;
private boolean stopping = false;
private int vibrationFilter = 0;
private String lastWakeupReason = "";
// Scheduler
private long watchdog = now();
private @Nullable ScheduledFuture<?> statusJob;
public int scheduledUpdates = 0;
protected int scheduledUpdates = 0;
private int skipCount = UPDATE_SKIP_COUNT;
private int skipUpdate = 0;
private boolean refreshSettings = false;
// delay before enabling channel
private final int cacheCount = UPDATE_SETTINGS_INTERVAL_SECONDS / UPDATE_STATUS_INTERVAL_SECONDS;
private final ShellyChannelCache cache;
private String lastWakeupReason = "";
private int vibrationFilter = 0;
private @Nullable ScheduledFuture<?> statusJob;
/**
* Constructor
@ -155,26 +141,28 @@ public class ShellyBaseHandler extends BaseThingHandler
this.channelDefinitions = new ShellyChannelDefinitions(messages);
this.bindingConfig = bindingConfig;
this.config = getConfigAs(ShellyThingConfiguration.class);
this.httpClient = httpClient;
this.api = new Shelly1HttpApi(thingName, config, httpClient);
coap = new Shelly1CoapHandler(this, coapServer);
Map<String, String> properties = thing.getProperties();
String gen = getString(properties.get(PROPERTY_DEV_GEN));
String thingType = getThingType();
if (gen.isEmpty() && thingType.startsWith("shellyplus") || thingType.startsWith("shellypro")) {
gen = "2";
}
gen2 = "2".equals(gen);
this.api = !gen2 ? new Shelly1HttpApi(thingName, this) : new Shelly2ApiRpc(thingName, thingTable, this);
if (gen2) {
config.eventsCoIoT = false;
}
if (config.eventsCoIoT) {
this.coap = new Shelly1CoapHandler(this, coapServer);
}
}
@Override
public boolean checkRepresentation(String key) {
return key.equalsIgnoreCase(getUID()) || key.equalsIgnoreCase(config.deviceIp)
|| key.equalsIgnoreCase(config.serviceName) || key.equalsIgnoreCase(thing.getUID().getAsString());
}
@Override
public Collection<Class<? extends ThingHandlerService>> getServices() {
return Set.of(ShellyStateDescriptionProvider.class);
}
public String getUID() {
return getThing().getUID().getAsString();
|| key.equalsIgnoreCase(config.serviceName) || key.equalsIgnoreCase(getThingName());
}
/**
@ -199,6 +187,10 @@ public class ShellyBaseHandler extends BaseThingHandler
start = initializeThing();
} catch (ShellyApiException e) {
ShellyApiResult res = e.getApiResult();
if (profile.alwaysOn && e.isConnectionError()) {
setThingOffline(ThingStatusDetail.COMMUNICATION_ERROR, "offline.status-error-connect",
e.toString());
}
if (isAuthorizationFailed(res)) {
start = false;
}
@ -255,22 +247,25 @@ public class ShellyBaseHandler extends BaseThingHandler
lastWakeupReason = "";
cache.setThingName(thingName);
cache.clear();
resetStats();
logger.debug("{}: Start initializing thing {}, type {}, ip address {}, CoIoT: {}", thingName,
getThing().getLabel(), thingType, config.deviceIp, config.eventsCoIoT);
logger.debug("{}: Start initializing for thing {}, type {}, IP address {}, Gen2: {}, CoIoT: {}", thingName,
getThing().getLabel(), thingType, config.deviceIp, gen2, config.eventsCoIoT);
if (config.deviceIp.isEmpty()) {
setThingOffline(ThingStatusDetail.CONFIGURATION_ERROR, "config-status.error.missing-device-ip");
return false;
}
// Setup CoAP listener to we get the CoAP message, which triggers initialization even the thing could not be
// fully initialized here. In this case the CoAP messages triggers auto-initialization (like the Action URL does
// when enabled)
if (config.eventsCoIoT && !profile.alwaysOn) {
// Gen 1 only: Setup CoAP listener to we get the CoAP message, which triggers initialization even the thing
// could not be fully initialized here. In this case the CoAP messages triggers auto-initialization (like the
// Action URL does when enabled)
if (coap != null && config.eventsCoIoT && !profile.alwaysOn) {
coap.start(thingName, config);
}
// Initialize API access, exceptions will be catched by initialize()
api.initialize();
profile.initFromThingType(thingType);
ShellySettingsDevice devInfo = api.getDeviceInfo();
if (getBool(devInfo.auth) && config.password.isEmpty()) {
setThingOffline(ThingStatusDetail.CONFIGURATION_ERROR, "offline.conf-error-no-credentials");
@ -281,8 +276,10 @@ public class ShellyBaseHandler extends BaseThingHandler
}
api.setConfig(thingName, config);
api.initialize();
ShellyDeviceProfile tmpPrf = api.getDeviceProfile(thingType);
tmpPrf.isGen2 = gen2;
tmpPrf.auth = devInfo.auth; // missing in /settings
if (this.getThing().getThingTypeUID().equals(THING_TYPE_SHELLYPROTECTED)) {
changeThingType(thingName, tmpPrf.mode);
return false; // force re-initialization
@ -290,7 +287,8 @@ public class ShellyBaseHandler extends BaseThingHandler
// Validate device mode
String reqMode = thingType.contains("-") ? substringAfter(thingType, "-") : "";
if (!reqMode.isEmpty() && !tmpPrf.mode.equals(reqMode)) {
setThingOffline(ThingStatusDetail.CONFIGURATION_ERROR, "offline.conf-error-wrong-mode");
setThingOffline(ThingStatusDetail.CONFIGURATION_ERROR, "offline.conf-error-wrong-mode", tmpPrf.mode,
reqMode);
return false;
}
if (!getString(devInfo.coiot).isEmpty()) {
@ -311,82 +309,32 @@ public class ShellyBaseHandler extends BaseThingHandler
tmpPrf.updatePeriod = UPDATE_SETTINGS_INTERVAL_SECONDS + 10;
}
tmpPrf.auth = devInfo.auth; // missing in /settings
tmpPrf.status = api.getStatus();
tmpPrf.status = api.getStatus(); // update thing properties
tmpPrf.updateFromStatus(tmpPrf.status);
addStateOptions(tmpPrf);
if (tmpPrf.isTRV) {
String[] profileNames = tmpPrf.getValveProfileList(0);
String channelId = mkChannelId(CHANNEL_GROUP_CONTROL, CHANNEL_CONTROL_PROFILE);
logger.debug("{}: Adding TRV profile names to channel description: {}", thingName, profileNames);
clearStateOptions(channelId);
addStateOption(channelId, "0", "DISABLED");
for (int i = 0; i < profileNames.length; i++) {
addStateOption(channelId, "" + (i + 1), profileNames[i]);
}
}
showThingConfig(tmpPrf);
// update thing properties
updateProperties(tmpPrf, tmpPrf.status);
checkVersion(tmpPrf, tmpPrf.status);
if (config.eventsCoIoT && (tmpPrf.settings.coiot != null) && (tmpPrf.settings.coiot.enabled != null)) {
String devpeer = getString(tmpPrf.settings.coiot.peer);
String ourpeer = config.localIp + ":" + Shelly1CoapJSonDTO.COIOT_PORT;
if (!tmpPrf.settings.coiot.enabled || (profile.isMotion && devpeer.isEmpty())) {
try {
api.setCoIoTPeer(ourpeer);
logger.info("{}: CoIoT peer updated to {}", thingName, ourpeer);
} catch (ShellyApiException e) {
logger.warn("{}: Unable to set CoIoT peer: {}", thingName, e.toString());
}
} else if (!devpeer.isEmpty() && !devpeer.equals(ourpeer)) {
logger.warn("{}: CoIoT peer in device settings does not point this to this host", thingName);
}
}
if (autoCoIoT) {
logger.debug("{}: Auto-CoIoT is enabled, disabling action urls", thingName);
config.eventsCoIoT = true;
config.eventsSwitch = false;
config.eventsButton = false;
config.eventsPush = false;
config.eventsRoller = false;
config.eventsSensorReport = false;
api.setConfig(thingName, config);
startCoap(config, tmpPrf);
if (!gen2) {
api.setActionURLs(); // register event urls
}
// All initialization done, so keep the profile and set Thing to ONLINE
fillDeviceStatus(tmpPrf.status, false);
postEvent(ALARM_TYPE_NONE, false);
api.setActionURLs(); // register event urls
if (config.eventsCoIoT) {
logger.debug("{}: Starting CoIoT (autoCoIoT={}/{})", thingName, bindingConfig.autoCoIoT, autoCoIoT);
coap.start(thingName, config);
}
profile = tmpPrf;
showThingConfig(profile);
logger.debug("{}: Thing successfully initialized.", thingName);
profile = tmpPrf;
updateProperties(tmpPrf, tmpPrf.status);
updateProperties(profile, profile.status);
setThingOnline(); // if API call was successful the thing must be online
return true; // success
}
private void showThingConfig(ShellyDeviceProfile profile) {
logger.debug("{}: Initializing device {}, type {}, Hardware: Rev: {}, batch {}; Firmware: {} / {}", thingName,
profile.hostname, profile.deviceType, profile.hwRev, profile.hwBatchId, profile.fwVersion,
profile.fwDate);
logger.debug("{}: Shelly settings info for {}: {}", thingName, profile.hostname, profile.settingsJson);
logger.debug("{}: Device "
+ "hasRelays:{} (numRelays={}),isRoller:{} (numRoller={}),isDimmer:{},numMeter={},isEMeter:{})"
+ ",isSensor:{},isDS:{},hasBattery:{}{},isSense:{},isMotion:{},isLight:{},isBulb:{},isDuo:{},isRGBW2:{},inColor:{}"
+ ",alwaysOn:{}, updatePeriod:{}sec", thingName, profile.hasRelays, profile.numRelays, profile.isRoller,
profile.numRollers, profile.isDimmer, profile.numMeters, profile.isEMeter, profile.isSensor,
profile.isDW, profile.hasBattery,
profile.hasBattery ? " (low battery threshold=" + config.lowBattery + "%)" : "", profile.isSense,
profile.isMotion, profile.isLight, profile.isBulb, profile.isDuo, profile.isRGBW2, profile.inColor,
profile.alwaysOn, profile.updatePeriod);
}
/**
* Handle Channel Commands
*/
@ -443,17 +391,31 @@ public class ShellyBaseHandler extends BaseThingHandler
break;
case CHANNEL_CONTROL_PROFILE:
logger.debug("{}: Select profile {}", thingName, command);
int id = -1;
if (command instanceof Number) {
id = (int) getNumber(command);
} else {
String cmd = command.toString();
int id = Integer.parseInt(cmd);
if (isDigit(cmd.charAt(0))) {
id = Integer.parseInt(cmd);
} else if (profile.settings.thermostats != null) {
ShellyThermnostat t = profile.settings.thermostats.get(0);
for (int i = 0; i < t.profileNames.length; i++) {
if (t.profileNames[i].equalsIgnoreCase(cmd)) {
id = i + 1;
}
}
}
}
if (id < 0 || id > 5) {
logger.warn("{}: Invalid profile Id {} requested", thingName, profile);
} else {
api.setValveProfile(0, id);
break;
}
api.setValveProfile(0, id);
break;
case CHANNEL_CONTROL_MODE:
logger.debug("{}: Set mode to {}", thingName, command);
api.setValveMode(0, SHELLY_TRV_MODE_AUTO.equalsIgnoreCase(command.toString()));
api.setValveMode(0, CHANNEL_CONTROL_MODE.equalsIgnoreCase(command.toString()));
break;
case CHANNEL_CONTROL_SETTEMP:
logger.debug("{}: Set temperature to {}", thingName, command);
@ -519,7 +481,7 @@ public class ShellyBaseHandler extends BaseThingHandler
if (vibrationFilter > 0) {
vibrationFilter--;
logger.debug("{}: Vibration events are absorbed for {} more seconds", thingName,
logger.debug("{}: Vibration events are absorbed for {} more seconds", thingName,
vibrationFilter * UPDATE_STATUS_INTERVAL_SECONDS);
}
@ -533,6 +495,9 @@ public class ShellyBaseHandler extends BaseThingHandler
}
// Get profile, if refreshSettings == true reload settings from device
ShellySettingsStatus status = api.getStatus();
if (status.uptime != null && status.uptime == 0 && profile.alwaysOn) {
status = api.getStatus();
}
boolean restarted = checkRestarted(status);
profile = getProfile(refreshSettings || restarted);
profile.status = status;
@ -563,7 +528,11 @@ public class ShellyBaseHandler extends BaseThingHandler
// sleep mode. Once the next update is successful the device goes back online
String status = "";
ShellyApiResult res = e.getApiResult();
if (isWatchdogStarted()) {
if (profile.alwaysOn && e.isConnectionError()) {
status = "offline.status-error-connect";
} else if (res.isHttpAccessUnauthorized()) {
status = "offline.conf-error-access-denied";
} else if (isWatchdogStarted()) {
if (!isWatchdogExpired()) {
logger.debug("{}: Ignore API Timeout, retry later", thingName);
} else {
@ -571,8 +540,6 @@ public class ShellyBaseHandler extends BaseThingHandler
status = "offline.status-error-watchdog";
}
}
} else if (res.isHttpAccessUnauthorized()) {
status = "offline.conf-error-access-denied";
} else if (e.isJSONException()) {
status = "offline.status-error-unexpected-api-result";
logger.debug("{}: Unable to parse API response: {}; json={}", thingName, res.getUrl(), res.response, e);
@ -603,31 +570,73 @@ public class ShellyBaseHandler extends BaseThingHandler
}
}
private void showThingConfig(ShellyDeviceProfile profile) {
logger.debug("{}: Initializing device {}, type {}, Hardware: Rev: {}, batch {}; Firmware: {} / {}", thingName,
profile.hostname, profile.deviceType, profile.hwRev, profile.hwBatchId, profile.fwVersion,
profile.fwDate);
logger.debug("{}: Shelly settings info for {}: {}", thingName, profile.hostname, profile.settingsJson);
logger.debug("{}: Device "
+ "hasRelays:{} (numRelays={}),isRoller:{} (numRoller={}),isDimmer:{},numMeter={},isEMeter:{})"
+ ",isSensor:{},isDS:{},hasBattery:{}{},isSense:{},isMotion:{},isLight:{},isBulb:{},isDuo:{},isRGBW2:{},inColor:{}"
+ ",alwaysOn:{}, updatePeriod:{}sec", thingName, profile.hasRelays, profile.numRelays, profile.isRoller,
profile.numRollers, profile.isDimmer, profile.numMeters, profile.isEMeter, profile.isSensor,
profile.isDW, profile.hasBattery,
profile.hasBattery ? " (low battery threshold=" + config.lowBattery + "%)" : "", profile.isSense,
profile.isMotion, profile.isLight, profile.isBulb, profile.isDuo, profile.isRGBW2, profile.inColor,
profile.alwaysOn, profile.updatePeriod);
}
private void addStateOptions(ShellyDeviceProfile prf) {
if (prf.isTRV) {
String[] profileNames = prf.getValveProfileList(0);
String channelId = mkChannelId(CHANNEL_GROUP_CONTROL, CHANNEL_CONTROL_PROFILE);
logger.debug("{}: Adding TRV profile names to channel description: {}", thingName, profileNames);
channelDefinitions.clearStateOptions(channelId);
int fid = 1;
for (String name : profileNames) {
channelDefinitions.addStateOption(channelId, "" + fid, fid + ": " + name);
fid++;
}
}
if (prf.isRoller && prf.settings.favorites != null) {
String channelId = mkChannelId(CHANNEL_GROUP_ROL_CONTROL, CHANNEL_ROL_CONTROL_FAV);
logger.debug("{}: Adding {} roler favorite(s) to channel description", thingName,
prf.settings.favorites.size());
channelDefinitions.clearStateOptions(channelId);
int fid = 1;
for (ShellyFavPos fav : prf.settings.favorites) {
channelDefinitions.addStateOption(channelId, "" + fid, fid + ": " + fav.name);
fid++;
}
}
}
@Override
public String getThingType() {
return thing.getThingTypeUID().getId();
}
@Override
public ThingStatus getThingStatus() {
return getThing().getStatus();
return thing.getStatus();
}
@Override
public ThingStatusDetail getThingStatusDetail() {
return getThing().getStatusInfo().getStatusDetail();
return thing.getStatusInfo().getStatusDetail();
}
@Override
public boolean isThingOnline() {
return getThing().getStatus() == ThingStatus.ONLINE;
return getThingStatus() == ThingStatus.ONLINE;
}
public boolean isThingOffline() {
return getThing().getStatus() == ThingStatus.OFFLINE;
return getThingStatus() == ThingStatus.OFFLINE;
}
@Override
public void setThingOnline() {
if (stopping) {
logger.debug("{}: Thing should go ONLINE, but handler is shutting down, ignore!", thingName);
return;
}
if (!isThingOnline()) {
updateStatus(ThingStatus.ONLINE);
@ -640,16 +649,10 @@ public class ShellyBaseHandler extends BaseThingHandler
}
@Override
public void setThingOffline(ThingStatusDetail detail, String messageKey) {
String message = messages.get(messageKey);
if (stopping) {
logger.debug("{}: Thing should go OFFLINE with status {}, but handler is shutting down -> ignore",
thingName, message);
return;
}
public void setThingOffline(ThingStatusDetail detail, String messageKey, Object... arguments) {
if (!isThingOffline()) {
updateStatus(ThingStatus.OFFLINE, detail, message);
updateStatus(ThingStatus.OFFLINE, detail, messages.get(messageKey, arguments));
api.close(); // Gen2: disconnect WS/close http sessions
watchdog = 0;
channelsCreated = false; // check for new channels after devices gets re-initialized (e.g. new
}
@ -723,15 +726,22 @@ public class ShellyBaseHandler extends BaseThingHandler
if (status.uptime != null) {
stats.lastUptime = getLong(status.uptime);
}
if (coap != null) {
stats.coiotMessages = coap.getMessageCount();
stats.coiotErrors = coap.getErrorCount();
}
if (!alarm.isEmpty()) {
postEvent(alarm, false);
}
}
@Override
public void incProtMessages() {
stats.protocolMessages++;
}
@Override
public void incProtErrors() {
stats.protocolErrors++;
}
/**
* Check if device has restarted and needs a new Thing initialization
*
@ -741,8 +751,10 @@ public class ShellyBaseHandler extends BaseThingHandler
private boolean checkRestarted(ShellySettingsStatus status) {
if (profile.isInitialized() && profile.alwaysOn /* exclude battery powered devices */
&& (status.uptime != null && status.uptime < stats.lastUptime
|| !profile.status.update.oldVersion.isEmpty()
&& !status.update.oldVersion.equals(profile.status.update.oldVersion))) {
|| (!profile.status.update.oldVersion.isEmpty()
&& !status.update.oldVersion.equals(profile.status.update.oldVersion)))) {
logger.debug("{}: Device has been restarted, uptime={}/{}, firmware={}/{}", thingName, stats.lastUptime,
getLong(status.uptime), profile.status.update.oldVersion, status.update.oldVersion);
updateProperties(profile, status);
return true;
}
@ -785,6 +797,10 @@ public class ShellyBaseHandler extends BaseThingHandler
}
}
public boolean isUpdateScheduled() {
return scheduledUpdates > 0;
}
/**
* Callback for device events
*
@ -980,12 +996,13 @@ public class ShellyBaseHandler extends BaseThingHandler
if (version.checkBeta(getString(prf.fwVersion))) {
logger.info("{}: {}", prf.hostname, messages.get("versioncheck.beta", prf.fwVersion, prf.fwDate));
} else {
if ((version.compare(prf.fwVersion, SHELLY_API_MIN_FWVERSION) < 0) && !profile.isMotion) {
String minVersion = !gen2 ? SHELLY_API_MIN_FWVERSION : SHELLY2_API_MIN_FWVERSION;
if (version.compare(prf.fwVersion, minVersion) < 0) {
logger.warn("{}: {}", prf.hostname,
messages.get("versioncheck.tooold", prf.fwVersion, prf.fwDate, SHELLY_API_MIN_FWVERSION));
messages.get("versioncheck.tooold", prf.fwVersion, prf.fwDate, minVersion));
}
}
if (bindingConfig.autoCoIoT && ((version.compare(prf.fwVersion, SHELLY_API_MIN_FWCOIOT)) >= 0)
if (!gen2 && bindingConfig.autoCoIoT && ((version.compare(prf.fwVersion, SHELLY_API_MIN_FWCOIOT)) >= 0)
|| (prf.fwVersion.equalsIgnoreCase("production_test"))) {
if (!config.eventsCoIoT) {
logger.info("{}: {}", thingName, messages.get("versioncheck.autocoiot"));
@ -1001,6 +1018,50 @@ public class ShellyBaseHandler extends BaseThingHandler
}
}
public String checkForUpdate() {
try {
ShellyOtaCheckResult result = api.checkForUpdate();
return result.status;
} catch (ShellyApiException e) {
return "";
}
}
public void startCoap(ShellyThingConfiguration config, ShellyDeviceProfile profile) throws ShellyApiException {
if (coap == null || !config.eventsCoIoT) {
return;
}
if (profile.settings.coiot != null && profile.settings.coiot.enabled != null) {
String devpeer = getString(profile.settings.coiot.peer);
String ourpeer = config.localIp + ":" + Shelly1CoapJSonDTO.COIOT_PORT;
if (!profile.settings.coiot.enabled || (profile.isMotion && devpeer.isEmpty())) {
try {
api.setCoIoTPeer(ourpeer);
logger.info("{}: CoIoT peer updated to {}", thingName, ourpeer);
} catch (ShellyApiException e) {
logger.debug("{}: Unable to set CoIoT peer: {}", thingName, e.toString());
}
} else if (!devpeer.isEmpty() && !devpeer.equals(ourpeer)) {
logger.warn("{}: CoIoT peer in device settings does not point this to this host", thingName);
}
}
if (autoCoIoT) {
logger.debug("{}: Auto-CoIoT is enabled, disabling action urls", thingName);
config.eventsCoIoT = true;
config.eventsSwitch = false;
config.eventsButton = false;
config.eventsPush = false;
config.eventsRoller = false;
config.eventsSensorReport = false;
api.setConfig(thingName, config);
}
logger.debug("{}: Starting CoIoT (autoCoIoT={}/{})", thingName, bindingConfig.autoCoIoT, autoCoIoT);
if (coap != null) {
coap.start(thingName, config);
}
}
/**
* Checks the http response for authorization error.
* If the authorization failed the binding can't access the device settings and determine the thing type. In this
@ -1081,10 +1142,6 @@ public class ShellyBaseHandler extends BaseThingHandler
return false;
}
public boolean isUpdateScheduled() {
return scheduledUpdates > 0;
}
/**
* Map input states to channels
*
@ -1098,17 +1155,15 @@ public class ShellyBaseHandler extends BaseThingHandler
boolean updated = false;
if (status.inputs != null) {
if (!areChannelsCreated()) {
updateChannelDefinitions(ShellyChannelDefinitions.createInputChannels(thing, profile, status));
}
int idx = 0;
boolean multiInput = status.inputs.size() >= 2; // device has multiple SW (inputs)
boolean multiInput = !profile.isIX && status.inputs.size() >= 2; // device has multiple SW (inputs)
for (ShellyInputState input : status.inputs) {
String group = profile.getInputGroup(idx);
String suffix = multiInput ? profile.getInputSuffix(idx) : "";
if (!areChannelsCreated()) {
updateChannelDefinitions(
ShellyChannelDefinitions.createInputChannels(thing, profile, status, group));
}
updated |= updateChannel(group, CHANNEL_INPUT + suffix, getOnOff(input.input));
if (input.event != null) {
updated |= updateChannel(group, CHANNEL_STATUS_EVENTTYPE + suffix, getStringType(input.event));
@ -1248,12 +1303,14 @@ public class ShellyBaseHandler extends BaseThingHandler
*/
public void updateProperties(ShellyDeviceProfile profile, ShellySettingsStatus status) {
Map<String, Object> properties = fillDeviceProperties(profile);
properties.put(PROPERTY_SERVICE_NAME, config.serviceName);
String deviceName = getString(profile.settings.name);
properties.put(PROPERTY_SERVICE_NAME, config.serviceName);
properties.put(PROPERTY_DEV_GEN, "1");
if (!deviceName.isEmpty()) {
properties.put(PROPERTY_DEV_NAME, deviceName);
}
properties.put(PROPERTY_DEV_GEN, !profile.isGen2 ? "1" : "2");
// add status properties
if (status.wifiSta != null) {
@ -1372,32 +1429,13 @@ public class ShellyBaseHandler extends BaseThingHandler
}
@Override
public List<StateOption> getStateOptions(ChannelTypeUID uid) {
List<StateOption> options = new ArrayList<>();
for (OptionEntry oe : stateOptions) {
if (oe.uid.equals(uid)) {
options.add(new StateOption(oe.key, oe.value));
}
}
public @Nullable List<StateOption> getStateOptions(ChannelTypeUID uid) {
List<StateOption> options = channelDefinitions.getStateOptions(uid);
if (!options.isEmpty()) {
logger.debug("{}: Return {} state options for channel uid {}", thingName, options.size(), uid.getId());
}
return options;
}
private void addStateOption(String channelId, String key, String value) {
ChannelTypeUID uid = channelDefinitions.getChannelTypeUID(channelId);
stateOptions.addIfAbsent(new OptionEntry(uid, key, value));
}
private void clearStateOptions(String channelId) {
ChannelTypeUID uid = channelDefinitions.getChannelTypeUID(channelId);
for (OptionEntry oe : stateOptions) {
if (oe.uid.equals(uid)) {
stateOptions.remove(oe);
}
}
return null;
}
protected ShellyDeviceProfile getDeviceProfile() {
@ -1431,10 +1469,7 @@ public class ShellyBaseHandler extends BaseThingHandler
statusJob = null;
logger.debug("{}: Shelly statusJob stopped", thingName);
}
if (coap != null) {
coap.stop();
}
api.close();
profile.initialized = false;
}
@ -1443,6 +1478,7 @@ public class ShellyBaseHandler extends BaseThingHandler
*/
@Override
public void dispose() {
logger.debug("{}: Stopping Thing", thingName);
stopping = true;
stop();
super.dispose();
@ -1455,6 +1491,10 @@ public class ShellyBaseHandler extends BaseThingHandler
return false;
}
public String getUID() {
return getThing().getUID().getAsString();
}
/**
* Device specific handlers are overriding this method to do additional stuff
*/
@ -1483,22 +1523,13 @@ public class ShellyBaseHandler extends BaseThingHandler
return api;
}
public Map<String, String> getStatsProp() {
return stats.asProperties();
}
@Override
public long getScheduledUpdates() {
return scheduledUpdates;
}
public String checkForUpdate() {
try {
ShellyOtaCheckResult result = api.checkForUpdate();
return result.status;
} catch (ShellyApiException e) {
return "";
}
public Map<String, String> getStatsProp() {
return stats.asProperties();
}
@Override

View File

@ -33,8 +33,8 @@ public class ShellyDeviceStats {
public long alarms = 0;
public String lastAlarm = "";
public long lastAlarmTs = 0;
public long coiotMessages = 0;
public long coiotErrors = 0;
public long protocolMessages = 0;
public long protocolErrors = 0;
public int wifiRssi = 0;
public int maxInternalTemp = 0;
@ -48,8 +48,8 @@ public class ShellyDeviceStats {
prop.put("alarmCount", String.valueOf(alarms));
prop.put("lastAlarm", lastAlarm);
prop.put("lastAlarmTs", ShellyUtils.convertTimestamp(lastAlarmTs));
prop.put("coiotMessages", String.valueOf(coiotMessages));
prop.put("coiotErrors", String.valueOf(coiotErrors));
prop.put("protocolMessages", String.valueOf(protocolMessages));
prop.put("protocolErrors", String.valueOf(protocolErrors));
prop.put("wifiRssi", String.valueOf(wifiRssi));
return prop;
}

View File

@ -46,7 +46,11 @@ public interface ShellyManagerInterface {
public void setThingOnline();
public void setThingOffline(ThingStatusDetail detail, String messageKey);
public void setThingOffline(ThingStatusDetail detail, String messageKey, Object... arguments);
public boolean requestUpdates(int requestCount, boolean refreshSettings);
public void incProtMessages();
public void incProtErrors();
}

View File

@ -111,7 +111,9 @@ public class ShellyRelayHandler extends ShellyBaseHandler {
channelUID.getIdWithoutGroup().equals(CHANNEL_ROL_CONTROL_CONTROL));
// request updates the next 45sec to update roller position after it stopped
requestUpdates(autoCoIoT ? 1 : 45 / UPDATE_STATUS_INTERVAL_SECONDS, false);
if (!autoCoIoT && !profile.isGen2) {
requestUpdates(45 / UPDATE_STATUS_INTERVAL_SECONDS, false);
}
break;
case CHANNEL_ROL_CONTROL_FAV:
@ -129,11 +131,11 @@ public class ShellyRelayHandler extends ShellyBaseHandler {
case CHANNEL_TIMER_AUTOON:
logger.debug("{}: Set Auto-ON timer to {}", thingName, command);
api.setTimer(rIndex, SHELLY_TIMER_AUTOON, getNumber(command).intValue());
api.setAutoTimer(rIndex, SHELLY_TIMER_AUTOON, getNumber(command).doubleValue());
break;
case CHANNEL_TIMER_AUTOOFF:
logger.debug("{}: Set Auto-OFF timer to {}", thingName, command);
api.setTimer(rIndex, SHELLY_TIMER_AUTOOFF, getNumber(command).intValue());
api.setAutoTimer(rIndex, SHELLY_TIMER_AUTOOFF, getNumber(command).doubleValue());
break;
}
return true;
@ -241,7 +243,6 @@ public class ShellyRelayHandler extends ShellyBaseHandler {
position = shpos;
} else {
api.setRollerTurn(index, SHELLY_ALWD_ROLLER_TURN_OPEN);
position = SHELLY_MIN_ROLLER_POS;
}
} else if (command == UpDownType.DOWN || command == OnOffType.OFF
|| ((command instanceof DecimalType) && (((DecimalType) command).intValue() == 0))) {
@ -255,7 +256,6 @@ public class ShellyRelayHandler extends ShellyBaseHandler {
position = shpos;
} else {
api.setRollerTurn(index, SHELLY_ALWD_ROLLER_TURN_CLOSE);
position = SHELLY_MAX_ROLLER_POS;
}
}
} else if (command == StopMoveType.STOP) {
@ -283,18 +283,6 @@ public class ShellyRelayHandler extends ShellyBaseHandler {
logger.debug("{}: Changing roller position to {}", thingName, position);
api.setRollerPos(index, position);
}
if (position != -1) {
// make sure both are in sync
if (isControl) {
int pos = SHELLY_MAX_ROLLER_POS - Math.max(0, Math.min(position, SHELLY_MAX_ROLLER_POS));
logger.debug("{}: Set roller position for control channel to {}", thingName, pos);
updateChannel(groupName, CHANNEL_ROL_CONTROL_CONTROL, new PercentType(pos));
} else {
logger.debug("{}: Set roller position channel to {}", thingName, position);
updateChannel(groupName, CHANNEL_ROL_CONTROL_POS, new PercentType(position));
}
}
}
/**
@ -329,7 +317,6 @@ public class ShellyRelayHandler extends ShellyBaseHandler {
*/
public boolean updateRelays(ShellySettingsStatus status) throws ShellyApiException {
boolean updated = false;
if (profile.hasRelays && !profile.isDimmer) {
double voltage = -1;
if (status.voltage == null && profile.settings.supplyVoltage != null) {
@ -343,14 +330,12 @@ public class ShellyRelayHandler extends ShellyBaseHandler {
updated |= updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_VOLTAGE,
toQuantityType(voltage, DIGITS_VOLT, Units.VOLT));
}
}
if (profile.hasRelays && !profile.isRoller) {
if (!profile.isRoller) {
logger.trace("{}: Updating {} relay(s)", thingName, profile.numRelays);
for (int i = 0; i < status.relays.size(); i++) {
createRelayChannels(status.relays.get(i), i);
updated |= ShellyComponents.updateRelay(this, status, i);
i++;
}
} else {
// Check for Relay in Roller Mode
@ -361,7 +346,7 @@ public class ShellyRelayHandler extends ShellyBaseHandler {
updated |= ShellyComponents.updateRoller(this, roller, i);
}
}
}
return updated;
}

View File

@ -41,7 +41,7 @@ public interface ShellyThingInterface {
public ShellyDeviceProfile getProfile(boolean forceRefresh) throws ShellyApiException;
public List<StateOption> getStateOptions(ChannelTypeUID uid);
public @Nullable List<StateOption> getStateOptions(ChannelTypeUID uid);
public double getChannelDouble(String group, String channel);
@ -51,7 +51,9 @@ public interface ShellyThingInterface {
public void setThingOnline();
public void setThingOffline(ThingStatusDetail detail, String messageKey);
public void setThingOffline(ThingStatusDetail detail, String messageKey, Object... arguments);
public String getThingType();
public ThingStatus getThingStatus();
@ -110,4 +112,8 @@ public interface ShellyThingInterface {
public void fillDeviceStatus(ShellySettingsStatus status, boolean updated);
public boolean checkRepresentation(String key);
public void incProtMessages();
public void incProtErrors();
}

View File

@ -31,6 +31,9 @@ public class ShellyThingTable {
private Map<String, ShellyThingInterface> thingTable = new ConcurrentHashMap<>();
public void addThing(String key, ShellyThingInterface thing) {
if (thingTable.containsKey(key)) {
thingTable.remove(key);
}
thingTable.put(key, thing);
}

View File

@ -138,7 +138,7 @@ public class ShellyManagerActionPage extends ShellyManagerPage {
}
String peer = getString(profile.settings.coiot.peer);
boolean mcast = peer.isEmpty() || SHELLY_COIOT_MCAST.equalsIgnoreCase(peer);
boolean mcast = peer.isEmpty() || SHELLY_COIOT_MCAST.equalsIgnoreCase(peer) || profile.isMotion;
String newPeer = mcast ? localIp + ":" + Shelly1CoapJSonDTO.COIOT_PORT : SHELLY_COIOT_MCAST;
String displayPeer = mcast ? newPeer : "Multicast";
@ -252,7 +252,6 @@ public class ShellyManagerActionPage extends ShellyManagerPage {
refreshTimer = 3;
}
break;
case ACTION_ENAPROAMING:
case ACTION_DISAPROAMING:
enable = ACTION_ENAPROAMING.equalsIgnoreCase(action);
@ -269,6 +268,77 @@ public class ShellyManagerActionPage extends ShellyManagerPage {
refreshTimer = 3;
}
break;
case ACTION_ENRANGEEXT:
case ACTION_DISRANGEEXT:
enable = ACTION_ENRANGEEXT.equalsIgnoreCase(action);
if (!"yes".equalsIgnoreCase(update)) {
message = getMessage(
enable ? "action.setwifirangeext-enable" : "action.setwifirangeext-disable");
actionUrl = buildActionUrl(uid, action);
} else {
try {
boolean res = api.setWiFiRangeExtender(enable);
if (res) {
message = getMessageP("action.restart.info", MCINFO);
actionButtonLabel = "Ok";
actionUrl = buildActionUrl(uid, ACTION_RESTART);
} else {
message = getMessage("action.setwifirangeext-confirm", res ? "yes" : "no");
refreshTimer = 5;
}
} catch (ShellyApiException e) {
message = getMessage("action.setwifirangeext-failed", e.toString());
refreshTimer = 10;
}
}
break;
case ACTION_ENETHERNET:
case ACTION_DISETHERNET:
enable = ACTION_ENETHERNET.equalsIgnoreCase(action);
if (!"yes".equalsIgnoreCase(update)) {
message = getMessage(enable ? "action.setethernet-enable" : "action.setethernet-disable");
actionUrl = buildActionUrl(uid, action);
} else {
try {
boolean res = api.setEthernet(enable);
if (res) {
message = getMessageP("action.restart.info", MCINFO);
actionButtonLabel = "Ok";
actionUrl = buildActionUrl(uid, ACTION_RESTART);
} else {
message = getMessage("action.setethernet-confirm", res ? "yes" : "no");
refreshTimer = 5;
}
} catch (ShellyApiException e) {
message = getMessage("action.setethernet-failed", e.toString());
refreshTimer = 10;
}
}
break;
case ACTION_ENBLUETOOTH:
case ACTION_DISBLUETOOTH:
enable = ACTION_ENBLUETOOTH.equalsIgnoreCase(action);
if (!"yes".equalsIgnoreCase(update)) {
message = getMessage(enable ? "action.setbluetooth-enable" : "action.setbluetooth-disable");
actionUrl = buildActionUrl(uid, action);
} else {
try {
boolean res = api.setBluetooth(enable);
if (res) {
message = getMessageP("action.restart.info", MCINFO);
actionButtonLabel = "Ok";
actionUrl = buildActionUrl(uid, ACTION_RESTART);
} else {
message = getMessage("action.setbluetooth-confirm", res ? "yes" : "no");
refreshTimer = 5;
}
} catch (ShellyApiException e) {
message = getMessage("action.setbluetooth-failed", e.toString());
refreshTimer = 10;
}
}
break;
case ACTION_GETDEB:
case ACTION_GETDEB1:
@ -303,35 +373,51 @@ public class ShellyManagerActionPage extends ShellyManagerPage {
public static Map<String, String> getActions(ShellyDeviceProfile profile) {
Map<String, String> list = new LinkedHashMap<>();
boolean gen2 = profile.isGen2;
list.put(ACTION_RES_STATS, "Reset Statistics");
list.put(ACTION_RESTART, "Reboot Device");
if (gen2) {
list.put(ACTION_PROTECT, "Protect Device");
}
if ((profile.settings.coiot != null) && (profile.settings.coiot.peer != null) && !profile.isMotion) {
if ((profile.settings.coiot != null) && profile.settings.coiot.peer != null) {
boolean mcast = profile.settings.coiot.peer.isEmpty()
|| SHELLY_COIOT_MCAST.equalsIgnoreCase(profile.settings.coiot.peer);
|| SHELLY_COIOT_MCAST.equalsIgnoreCase(profile.settings.coiot.peer) || profile.isMotion;
list.put(mcast ? ACTION_SETCOIOT_PEER : ACTION_SETCOIOT_MCAST,
mcast ? "Set CoIoT Peer Mode" : "Set CoIoT Multicast Mode");
}
if (profile.isSensor && !profile.isMotion && (profile.settings.wifiSta != null)
if (profile.isSensor && !profile.isMotion && profile.settings.wifiSta != null
&& profile.settings.wifiSta.enabled) {
// FW 1.10+: Reset STA list, force WiFi rescan and connect to stringest AP
list.put(ACTION_RESSTA, "Reconnect WiFi");
}
if (profile.settings.apRoaming != null) {
if (!gen2 && profile.settings.apRoaming != null) {
list.put(!profile.settings.apRoaming.enabled ? ACTION_ENAPROAMING : ACTION_DISAPROAMING,
!profile.settings.apRoaming.enabled ? "Enable WiFi Roaming" : "Disable WiFi Roaming");
}
if (profile.settings.wifiRecoveryReboot != null) {
if (!gen2 && profile.settings.wifiRecoveryReboot != null) {
list.put(!profile.settings.wifiRecoveryReboot ? ACTION_ENWIFIREC : ACTION_DISWIFIREC,
!profile.settings.wifiRecoveryReboot ? "Enable WiFi Recovery" : "Disable WiFi Recovery");
}
if (profile.settings.wifiAp != null && profile.settings.wifiAp.rangeExtender != null) {
list.put(!profile.settings.wifiAp.rangeExtender ? ACTION_ENRANGEEXT : ACTION_DISRANGEEXT,
!profile.settings.wifiAp.rangeExtender ? "Enable Range Extender" : "Disable Range Extender");
}
if (profile.settings.ethernet != null) {
list.put(!profile.settings.ethernet ? ACTION_ENETHERNET : ACTION_DISETHERNET,
!profile.settings.ethernet ? "Enable Ethernet" : "Disable Ethernet");
}
if (profile.settings.bluetooth != null) {
list.put(!profile.settings.bluetooth ? ACTION_ENBLUETOOTH : ACTION_DISBLUETOOTH,
!profile.settings.bluetooth ? "Enable Bluetooth" : "Disable Bluetooth");
}
boolean set = profile.settings.cloud != null && profile.settings.cloud.enabled;
list.put(set ? ACTION_DISCLOUD : ACTION_ENCLOUD, set ? "Disable Cloud" : "Enable Cloud");
list.put(ACTION_RESET, "-Factory Reset");
if (profile.extFeatures) {
if (!gen2 && profile.extFeatures) {
list.put(ACTION_OTACHECK, "Check for Update");
boolean debug_enable = getBool(profile.settings.debugEnable);
list.put(!debug_enable ? ACTION_ENDEBUG : ACTION_DISDEBUG,

View File

@ -39,7 +39,6 @@ public class ShellyManagerConstants {
public static final String ACTION_SETCOIOT_PEER = "setcoiotpeer";
public static final String ACTION_SETCOIOT_MCAST = "setcoiotmcast";
public static final String ACTION_SETTZ = "settz";
public static final String ACTION_SETNTP = "setntp";
public static final String ACTION_ENCLOUD = "encloud";
public static final String ACTION_DISCLOUD = "discloud";
public static final String ACTION_RES_STATS = "reset_stat";
@ -49,6 +48,12 @@ public class ShellyManagerConstants {
public static final String ACTION_DISWIFIREC = "diswifirec";
public static final String ACTION_ENAPROAMING = "enaproaming";
public static final String ACTION_DISAPROAMING = "disaproaming";
public static final String ACTION_ENRANGEEXT = "enrangeext";
public static final String ACTION_ENETHERNET = "enethernet";
public static final String ACTION_DISETHERNET = "disethernet";
public static final String ACTION_ENBLUETOOTH = "enbluetooth";
public static final String ACTION_DISBLUETOOTH = "disbluetooth";
public static final String ACTION_DISRANGEEXT = "disrangeext";
public static final String ACTION_OTACHECK = "otacheck";
public static final String ACTION_ENDEBUG = "endebug";
public static final String ACTION_DISDEBUG = "disdebug";

View File

@ -33,9 +33,9 @@ import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.HttpStatus;
import org.openhab.binding.shelly.internal.ShellyHandlerFactory;
import org.openhab.binding.shelly.internal.api.ShellyApiException;
import org.openhab.binding.shelly.internal.api.ShellyApiInterface;
import org.openhab.binding.shelly.internal.api.ShellyDeviceProfile;
import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsUpdate;
import org.openhab.binding.shelly.internal.api1.Shelly1HttpApi;
import org.openhab.binding.shelly.internal.config.ShellyThingConfiguration;
import org.openhab.binding.shelly.internal.handler.ShellyManagerInterface;
import org.openhab.binding.shelly.internal.provider.ShellyTranslationProvider;
@ -122,7 +122,7 @@ public class ShellyManagerOtaPage extends ShellyManagerPage {
new Thread(() -> { // schedule asynchronous reboot
try {
Shelly1HttpApi api = new Shelly1HttpApi(uid, config, httpClient);
ShellyApiInterface api = th.getApi();
ShellySettingsUpdate result = api.firmwareUpdate(updateUrl);
String status = getString(result.status);
logger.info("{}: {}", th.getThingName(), getMessage("fwupdate.initiated", status));

View File

@ -111,7 +111,7 @@ public class ShellyManagerOverviewPage extends ShellyManagerPage {
properties.put(ATTRIBUTE_STATUS_ICON, ICON_ATTENTION);
}
if (!"unknown".equalsIgnoreCase(deviceType) && (status == ThingStatus.ONLINE)) {
properties.put(ATTRIBUTE_FIRMWARE_SEL, fillFirmwareHtml(uid, deviceType, profile.mode));
properties.put(ATTRIBUTE_FIRMWARE_SEL, fillFirmwareHtml(profile, uid, deviceType));
properties.put(ATTRIBUTE_ACTION_LIST, fillActionHtml(th, uid));
} else {
properties.put(ATTRIBUTE_FIRMWARE_SEL, "");
@ -132,7 +132,8 @@ public class ShellyManagerOverviewPage extends ShellyManagerPage {
return new ShellyMgrResponse(fillAttributes(html, properties), HttpStatus.OK_200);
}
private String fillFirmwareHtml(String uid, String deviceType, String mode) throws ShellyApiException {
private String fillFirmwareHtml(ShellyDeviceProfile profile, String uid, String deviceType)
throws ShellyApiException {
String html = "\n\t\t\t\t<select name=\"fwList\" id=\"fwList\" onchange=\"location = this.options[this.selectedIndex].value;\">\n";
html += "\t\t\t\t\t<option value=\"\" selected disabled hidden>update to</option>\n";
@ -140,20 +141,26 @@ public class ShellyManagerOverviewPage extends ShellyManagerPage {
String bVersion = "";
String updateUrl = SHELLY_MGR_FWUPDATE_URI + "?" + URLPARM_UID + "=" + urlEncode(uid);
try {
// Get current prod + beta version from original firmware repo
if (!profile.isGen2) { // currently there is no public firmware repo for Gen2
logger.debug("{}: Load firmware version list for device type {}", LOG_PREFIX, deviceType);
FwRepoEntry fw = getFirmwareRepoEntry(deviceType, mode);
FwRepoEntry fw = getFirmwareRepoEntry(deviceType, profile.mode);
pVersion = extractFwVersion(fw.version);
bVersion = extractFwVersion(fw.betaVer);
} else {
pVersion = extractFwVersion(getString(profile.status.update.newVersion));
bVersion = extractFwVersion(getString(profile.status.update.betaVersion));
}
if (!pVersion.isEmpty()) {
html += "\t\t\t\t\t<option value=\"" + updateUrl + "&" + URLPARM_VERSION + "=" + FWPROD + "\">Release "
+ pVersion + "</option>\n";
}
bVersion = extractFwVersion(fw.betaVer);
if (!bVersion.isEmpty()) {
html += "\t\t\t\t\t<option value=\"" + updateUrl + "&" + URLPARM_VERSION + "=" + FWBETA + "\">Beta "
+ bVersion + "</option>\n";
}
if (!profile.isGen2) { // currently no online repo for Gen2
// Add those from Shelly Firmware Archive
String json = httpGet(FWREPO_ARCH_URL + "?" + URLPARM_TYPE + "=" + deviceType);
if (json.startsWith("[]")) {
@ -171,14 +178,18 @@ public class ShellyManagerOverviewPage extends ShellyManagerPage {
String version = getString(e.version);
ShellyVersionDTO v = new ShellyVersionDTO();
if (!version.equalsIgnoreCase(pVersion) && !version.equalsIgnoreCase(bVersion)
&& (v.compare(version, SHELLY_API_MIN_FWCOIOT) >= 0) || version.contains("master")) {
&& (v.compare(version, SHELLY_API_MIN_FWCOIOT) >= 0)
|| version.contains("master")) {
html += "\t\t\t\t\t<option value=\"" + updateUrl + "&" + URLPARM_VERSION + "=" + version
+ "\">" + version + "</option>\n";
}
}
}
}
} catch (ShellyApiException e) {
}
} catch (
ShellyApiException e) {
logger.debug("{}: Unable to retrieve firmware list: {}", LOG_PREFIX, e.toString());
}
@ -282,9 +293,9 @@ public class ShellyManagerOverviewPage extends ShellyManagerPage {
if ((config.eventsCoIoT) && (profile.settings.coiot != null)) {
if ((profile.settings.coiot.enabled != null) && !profile.settings.coiot.enabled) {
result.put("CoIoT Status", "COIOT_DISABLED");
} else if (stats.coiotMessages == 0) {
} else if (stats.protocolMessages == 0) {
result.put("CoIoT Discovery", "NO_COIOT_DISCOVERY");
} else if (stats.coiotMessages < 2) {
} else if (stats.protocolMessages < 2) {
result.put("CoIoT Multicast", "NO_COIOT_MULTICAST");
}
}

View File

@ -261,6 +261,9 @@ public class ShellyManagerPage {
properties.put(ATTRIBUTE_ACTIONS_SKIPPED,
profile.status.astats != null ? String.valueOf(profile.status.astats.skipped) : "n/a");
properties.put(ATTRIBUTE_MAX_ITEMP, stats.maxInternalTemp > 0 ? stats.maxInternalTemp + " °C" : "n/a");
if (stats.maxInternalTemp == 0) {
properties.replace(CHANNEL_DEVST_ITEMP, "n/a");
}
// Shelly H&T: When external power is connected the battery level is not valid
if (!profile.isHT || (getInteger(profile.settings.externalPower) == 0)) {

View File

@ -16,12 +16,15 @@ import static org.openhab.binding.shelly.internal.ShellyBindingConstants.*;
import static org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.SHELLY_API_INVTEMP;
import static org.openhab.binding.shelly.internal.util.ShellyUtils.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;
import javax.measure.Unit;
@ -47,6 +50,7 @@ import org.openhab.core.thing.Thing;
import org.openhab.core.thing.binding.builder.ChannelBuilder;
import org.openhab.core.thing.type.ChannelKind;
import org.openhab.core.thing.type.ChannelTypeUID;
import org.openhab.core.types.StateOption;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
@ -97,6 +101,20 @@ public class ShellyChannelDefinitions {
public static final String PREFIX_GROUP = "group-type." + BINDING_ID + ".";
public static final String PREFIX_CHANNEL = "channel-type." + BINDING_ID + ".";
public class OptionEntry {
public ChannelTypeUID uid;
public String key;
public String value;
public OptionEntry(ChannelTypeUID uid, String key, String value) {
this.uid = uid;
this.key = key;
this.value = value;
}
}
private final CopyOnWriteArrayList<OptionEntry> stateOptions = new CopyOnWriteArrayList<>();
private static final ChannelMap CHANNEL_DEFINITIONS = new ChannelMap();
@Activate
@ -169,8 +187,6 @@ public class ShellyChannelDefinitions {
.add(new ShellyChannel(m, CHGR_LIGHTCH, CHANNEL_TIMER_AUTOOFF, "timerAutoOff", ITEMT_TIME))
.add(new ShellyChannel(m, CHGR_LIGHTCH, CHANNEL_TIMER_ACTIVE, "timerActive", ITEMT_SWITCH))
// RGBW2-color
.add(new ShellyChannel(m, CHGR_LIGHT, CHANNEL_LIGHT_POWER, "system:power", ITEMT_SWITCH))
// Power Meter
.add(new ShellyChannel(m, CHGR_METER, CHANNEL_METER_CURRENTWATTS, "meterWatts", ITEMT_POWER))
.add(new ShellyChannel(m, CHGR_METER, CHANNEL_METER_TOTALKWH, "meterTotal", ITEMT_ENERGY))
@ -182,7 +198,7 @@ public class ShellyChannelDefinitions {
.add(new ShellyChannel(m, CHGR_METER, CHANNEL_EMETER_REACTWATTS, "meterReactive", ITEMT_POWER))
.add(new ShellyChannel(m, CHGR_METER, CHANNEL_EMETER_VOLTAGE, "meterVoltage", ITEMT_VOLT))
.add(new ShellyChannel(m, CHGR_METER, CHANNEL_EMETER_CURRENT, "meterCurrent", ITEMT_AMP))
.add(new ShellyChannel(m, CHGR_METER, CHANNEL_EMETER_PFACTOR, "meterPowerFactor", ITEMT_PERCENT))
.add(new ShellyChannel(m, CHGR_METER, CHANNEL_EMETER_PFACTOR, "meterPowerFactor", ITEMT_NUMBER))
// Sensors
.add(new ShellyChannel(m, CHGR_SENSOR, CHANNEL_SENSOR_TEMP, "sensorTemp", ITEMT_TEMP))
@ -227,11 +243,11 @@ public class ShellyChannelDefinitions {
// TRV
.add(new ShellyChannel(m, CHGR_CONTROL, CHANNEL_CONTROL_POSITION, "sensorPosition", ITEMT_DIMMER))
.add(new ShellyChannel(m, CHGR_CONTROL, CHANNEL_CONTROL_MODE, "controlMode", ITEMT_STRING))
.add(new ShellyChannel(m, CHGR_CONTROL, CHANNEL_CONTROL_SCHEDULE, "controlSchedule", ITEMT_SWITCH))
.add(new ShellyChannel(m, CHGR_CONTROL, CHANNEL_CONTROL_PROFILE, "controlProfile", ITEMT_STRING))
.add(new ShellyChannel(m, CHGR_CONTROL, CHANNEL_CONTROL_SETTEMP, "targetTemp", ITEMT_TEMP))
.add(new ShellyChannel(m, CHGR_CONTROL, CHANNEL_CONTROL_BCONTROL, "boostControl", ITEMT_SWITCH))
.add(new ShellyChannel(m, CHGR_CONTROL, CHANNEL_CONTROL_BTIMER, "boostTimer", ITEMT_TIME));
.add(new ShellyChannel(m, CHGR_CONTROL, CHANNEL_CONTROL_BTIMER, "boostTimer", ITEMT_TIME))
.add(new ShellyChannel(m, CHGR_CONTROL, CHANNEL_CONTROL_SCHEDULE, "controlSchedule", ITEMT_SWITCH));
}
public static @Nullable ShellyChannel getDefinition(String channelName) throws IllegalArgumentException {
@ -273,7 +289,8 @@ public class ShellyChannelDefinitions {
addChannel(thing, add, profile.settings.name != null, CHGR_DEVST, CHANNEL_DEVST_NAME);
if (!profile.isSensor && !profile.isIX && getDouble(status.temperature) != SHELLY_API_INVTEMP) {
if (!profile.isSensor && !profile.isIX && status.temperature != null
&& status.temperature != SHELLY_API_INVTEMP) {
// Only some devices report the internal device temp
addChannel(thing, add, status.tmp != null || status.temperature != null, CHGR_DEVST, CHANNEL_DEVST_ITEMP);
}
@ -311,19 +328,20 @@ public class ShellyChannelDefinitions {
if (profile.settings.relays != null) {
ShellySettingsRelay rs = profile.settings.relays.get(idx);
boolean timer = rs.hasTimer != null || rstatus.hasTimer != null; // Dimmer 1/2 have
addChannel(thing, add, rs.ison != null, group, CHANNEL_OUTPUT);
addChannel(thing, add, rs.name != null, group, CHANNEL_OUTPUT_NAME);
boolean timer = rs.hasTimer != null || rstatus.hasTimer != null; // Dimmer 1/2 have
addChannel(thing, add, timer, group, CHANNEL_TIMER_ACTIVE);
addChannel(thing, add, rs.autoOn != null, group, CHANNEL_TIMER_AUTOON);
addChannel(thing, add, rs.autoOff != null, group, CHANNEL_TIMER_AUTOOFF);
addChannel(thing, add, timer, group, CHANNEL_TIMER_ACTIVE);
}
// Shelly 1/1PM Addon
if (profile.settings.extTemperature != null) {
addChannel(thing, add, profile.settings.extTemperature.sensor1 != null, CHGR_SENSOR, CHANNEL_ESENDOR_TEMP1);
addChannel(thing, add, profile.settings.extTemperature.sensor2 != null, CHGR_SENSOR, CHANNEL_ESENDOR_TEMP2);
addChannel(thing, add, profile.settings.extTemperature.sensor3 != null, CHGR_SENSOR, CHANNEL_ESENDOR_TEMP3);
if (profile.status.extTemperature != null) {
addChannel(thing, add, profile.status.extTemperature.sensor1 != null, CHGR_SENSOR, CHANNEL_ESENDOR_TEMP1);
addChannel(thing, add, profile.status.extTemperature.sensor2 != null, CHGR_SENSOR, CHANNEL_ESENDOR_TEMP2);
addChannel(thing, add, profile.status.extTemperature.sensor3 != null, CHGR_SENSOR, CHANNEL_ESENDOR_TEMP3);
}
if (profile.settings.extHumidity != null) {
addChannel(thing, add, profile.settings.extHumidity.sensor1 != null, CHGR_SENSOR, CHANNEL_ESENDOR_HUMIDITY);
@ -344,7 +362,6 @@ public class ShellyChannelDefinitions {
ShellySettingsDimmer ds = profile.settings.dimmers.get(idx);
addChannel(thing, add, ds.autoOn != null, group, CHANNEL_TIMER_AUTOON);
addChannel(thing, add, ds.autoOff != null, group, CHANNEL_TIMER_AUTOOFF);
ShellyShortLightStatus dss = dstatus.dimmers.get(idx);
addChannel(thing, add, dss != null && dss.hasTimer != null, group, CHANNEL_TIMER_ACTIVE);
}
@ -359,7 +376,6 @@ public class ShellyChannelDefinitions {
if (profile.settings.lights != null) {
ShellySettingsRgbwLight light = profile.settings.lights.get(idx);
String whiteGroup = profile.isRGBW2 ? group : CHANNEL_GROUP_WHITE_CONTROL;
// Create power channel in color mode and brightness channel in white mode
addChannel(thing, add, profile.inColor, group, CHANNEL_LIGHT_POWER);
addChannel(thing, add, light.autoOn != null, group, CHANNEL_TIMER_AUTOON);
@ -368,29 +384,31 @@ public class ShellyChannelDefinitions {
addChannel(thing, add, light.brightness != null, whiteGroup, CHANNEL_BRIGHTNESS);
addChannel(thing, add, light.temp != null, whiteGroup, CHANNEL_COLOR_TEMP);
}
return add;
}
public static Map<String, Channel> createInputChannels(final Thing thing, final ShellyDeviceProfile profile,
final ShellySettingsStatus status, String group) {
final ShellySettingsStatus status) {
Map<String, Channel> add = new LinkedHashMap<>();
if (status.inputs != null) {
// Create channels per input. For devices with more than 1 input (Dimmer, 1L) multiple channel sets are
// created by adding the index to the channel name
boolean multi = (profile.numRelays == 1 || profile.isDimmer || profile.isRoller) && profile.numInputs >= 2;
for (int i = 0; i < profile.numInputs; i++) {
String suffix = multi ? String.valueOf(i + 1) : "";
ShellyInputState input = status.inputs.get(i);
String group = profile.getInputGroup(i);
String suffix = profile.getInputSuffix(i); // multi ? String.valueOf(i + 1) : "";
addChannel(thing, add, true, group, CHANNEL_INPUT + suffix);
addChannel(thing, add, true, group,
(!profile.isRoller ? CHANNEL_BUTTON_TRIGGER + suffix : CHANNEL_EVENT_TRIGGER));
if (profile.inButtonMode(i)) {
ShellyInputState input = status.inputs.get(i);
addChannel(thing, add, input.event != null, group, CHANNEL_STATUS_EVENTTYPE + suffix);
addChannel(thing, add, input.eventCount != null, group, CHANNEL_STATUS_EVENTCOUNT + suffix);
}
addChannel(thing, add, true, group,
(!profile.isRoller ? CHANNEL_BUTTON_TRIGGER + suffix : CHANNEL_EVENT_TRIGGER));
}
} else if (status.input != null) {
// old RGBW2 firmware
String group = profile.getInputGroup(0);
addChannel(thing, add, true, group, CHANNEL_INPUT);
addChannel(thing, add, true, group, CHANNEL_BUTTON_TRIGGER);
}
@ -409,7 +427,7 @@ public class ShellyChannelDefinitions {
ShellyThingInterface handler = (ShellyThingInterface) thing.getHandler();
if (handler != null) {
ShellySettingsGlobal settings = handler.getProfile().settings;
if (getBool(settings.favoritesEnabled) && (settings.favorites != null)) {
if (getBool(settings.favoritesEnabled) && settings.favorites != null) {
addChannel(thing, add, roller.currentPos != null, CHGR_ROLLER, CHANNEL_ROL_CONTROL_FAV);
}
}
@ -435,7 +453,7 @@ public class ShellyChannelDefinitions {
addChannel(thing, newChannels, emeter.reactive != null, group, CHANNEL_EMETER_REACTWATTS);
addChannel(thing, newChannels, emeter.voltage != null, group, CHANNEL_EMETER_VOLTAGE);
addChannel(thing, newChannels, emeter.current != null, group, CHANNEL_EMETER_CURRENT);
addChannel(thing, newChannels, emeter.power != null, group, CHANNEL_EMETER_PFACTOR); // EM has no PF. but power
addChannel(thing, newChannels, emeter.pf != null, group, CHANNEL_EMETER_PFACTOR); // EM has no PF. but power
addChannel(thing, newChannels, true, group, CHANNEL_LAST_UPDATE);
return newChannels;
@ -498,7 +516,7 @@ public class ShellyChannelDefinitions {
addChannel(thing, newChannels, true, CHANNEL_GROUP_CONTROL, CHANNEL_CONTROL_MODE);
addChannel(thing, newChannels, true, CHANNEL_GROUP_CONTROL, CHANNEL_CONTROL_PROFILE);
addChannel(thing, newChannels, true, CHANNEL_GROUP_CONTROL, CHANNEL_CONTROL_SCHEDULE);
addChannel(thing, newChannels, true, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_STATE); // TRV
addChannel(thing, newChannels, true, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_STATE);
}
// Battery
@ -550,6 +568,30 @@ public class ShellyChannelDefinitions {
}
}
public List<StateOption> getStateOptions(ChannelTypeUID uid) {
List<StateOption> options = new ArrayList<>();
for (OptionEntry oe : stateOptions) {
if (oe.uid.equals(uid)) {
options.add(new StateOption(oe.key, oe.value));
}
}
return options;
}
public void addStateOption(String channelId, String key, String value) {
ChannelTypeUID uid = getChannelTypeUID(channelId);
stateOptions.addIfAbsent(new OptionEntry(uid, key, value));
}
public void clearStateOptions(String channelId) {
ChannelTypeUID uid = getChannelTypeUID(channelId);
for (OptionEntry oe : stateOptions) {
if (oe.uid.equals(uid)) {
stateOptions.remove(oe);
}
}
}
public class ShellyChannel {
private final ShellyTranslationProvider messages;
public String group = "";

View File

@ -13,17 +13,29 @@
package org.openhab.binding.shelly.internal.provider;
import static org.openhab.binding.shelly.internal.ShellyBindingConstants.BINDING_ID;
import java.util.List;
import java.util.Locale;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.shelly.internal.handler.ShellyThingInterface;
import org.openhab.core.events.EventPublisher;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingRegistry;
import org.openhab.core.thing.binding.BaseDynamicStateDescriptionProvider;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerService;
import org.openhab.core.thing.i18n.ChannelTypeI18nLocalizationService;
import org.openhab.core.thing.link.ItemChannelLinkRegistry;
import org.openhab.core.thing.type.ChannelTypeUID;
import org.openhab.core.thing.type.DynamicStateDescriptionProvider;
import org.openhab.core.types.StateDescription;
import org.openhab.core.types.StateDescriptionFragmentBuilder;
import org.openhab.core.types.StateOption;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
/**
* This class provides the list of valid inputs for the input channel of a source.
@ -32,27 +44,39 @@ import org.openhab.core.types.StateDescription;
*
*/
@NonNullByDefault
public class ShellyStateDescriptionProvider extends BaseDynamicStateDescriptionProvider implements ThingHandlerService {
private @Nullable ShellyThingInterface handler;
@Component(service = { DynamicStateDescriptionProvider.class, ShellyStateDescriptionProvider.class })
public class ShellyStateDescriptionProvider extends BaseDynamicStateDescriptionProvider {
private final ThingRegistry thingRegistry;
@Override
public void setThingHandler(ThingHandler handler) {
this.handler = (ShellyThingInterface) handler;
}
@Override
public @Nullable ThingHandler getThingHandler() {
return (ThingHandler) handler;
@Activate
public ShellyStateDescriptionProvider(final @Reference EventPublisher eventPublisher, //
final @Reference ItemChannelLinkRegistry itemChannelLinkRegistry, //
final @Reference ChannelTypeI18nLocalizationService channelTypeI18nLocalizationService,
@Reference ThingRegistry thingRegistry) {
this.eventPublisher = eventPublisher;
this.itemChannelLinkRegistry = itemChannelLinkRegistry;
this.channelTypeI18nLocalizationService = channelTypeI18nLocalizationService;
this.thingRegistry = thingRegistry;
}
@SuppressWarnings("null")
@Override
public @Nullable StateDescription getStateDescription(Channel channel, @Nullable StateDescription original,
@Nullable Locale locale) {
public @Nullable StateDescription getStateDescription(Channel channel,
@Nullable StateDescription originalStateDescription, @Nullable Locale locale) {
ChannelTypeUID uid = channel.getChannelTypeUID();
if (uid != null && handler != null) {
setStateOptions(channel.getUID(), handler.getStateOptions(uid));
if (uid == null || !BINDING_ID.equals(uid.getBindingId()) || originalStateDescription == null) {
return null;
}
return super.getStateDescription(channel, original, locale);
Thing thing = thingRegistry.get(channel.getUID().getThingUID());
ShellyThingInterface handler = (ShellyThingInterface) thing.getHandler();
if (handler == null) {
return null;
}
List<StateOption> stateOptions = handler.getStateOptions(uid);
return stateOptions == null ? null
: StateDescriptionFragmentBuilder.create(originalStateDescription).withOptions(stateOptions).build()
.toStateDescription();
}
}

View File

@ -0,0 +1,82 @@
<?xml version="1.0" encoding="UTF-8"?>
<config-description:config-descriptions
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0 https://openhab.org/schemas/config-description-1.0.0.xsd">
<config-description uri="thing-type:shelly:relay-gen2">
<parameter name="deviceIp" type="text" required="true">
<label>@text/thing-type.config.shelly.deviceIp.label</label>
<description>@text/thing-type.config.shelly.deviceIp.description</description>
<context>network-address</context>
</parameter>
<parameter name="password" type="text" required="false">
<label>@text/thing-type.config.shelly.password.label</label>
<description>@text/thing-type.config.shelly.password.description</description>
<context>password</context>
</parameter>
<parameter name="updateInterval" type="integer" min="10" required="true" unit="s">
<label>@text/thing-type.config.shelly.updateInterval.label</label>
<description>@text/thing-type.config.shelly.updateInterval.description</description>
<default>60</default>
<unitLabel>seconds</unitLabel>
<advanced>true</advanced>
</parameter>
</config-description>
<config-description uri="thing-type:shelly:roller-gen2">
<parameter name="deviceIp" type="text" required="true">
<label>@text/thing-type.config.shelly.deviceIp.label</label>
<description>@text/thing-type.config.shelly.deviceIp.description</description>
<context>network-address</context>
</parameter>
<parameter name="password" type="text" required="false">
<label>@text/thing-type.config.shelly.password.label</label>
<description>@text/thing-type.config.shelly.password.description</description>
<context>password</context>
</parameter>
<parameter name="favoriteUP" type="integer" min="0" max="4" required="false">
<label>@text/thing-type.config.shelly.roller.favoriteUP.label</label>
<description>@text/thing-type.config.shelly.roller.favoriteUP.description</description>
<default>0</default>
</parameter>
<parameter name="favoriteDOWN" type="integer" min="0" max="4" required="false">
<label>@text/thing-type.config.shelly.roller.favoriteDOWN.label</label>
<description>@text/thing-type.config.shelly.roller.favoriteDOWN.description</description>
<default>0</default>
</parameter>
<parameter name="updateInterval" type="integer" min="10" required="true" unit="s">
<label>@text/thing-type.config.shelly.updateInterval.label</label>
<description>@text/thing-type.config.shelly.updateInterval.description</description>
<default>60</default>
<unitLabel>seconds</unitLabel>
<advanced>true</advanced>
</parameter>
</config-description>
<config-description uri="thing-type:shelly:battery-gen2">
<parameter name="deviceIp" type="text" required="true">
<label>@text/thing-type.config.shelly.deviceIp.label</label>
<description>@text/thing-type.config.shelly.deviceIp.description</description>
<context>network-address</context>
</parameter>
<parameter name="password" type="text" required="false">
<label>@text/thing-type.config.shelly.password.label</label>
<description>@text/thing-type.config.shelly.password.description</description>
<context>password</context>
</parameter>
<parameter name="lowBattery" type="integer" required="false">
<label>@text/thing-type.config.shelly.battery.lowBattery.label</label>
<description>@text/thing-type.config.shelly.battery.lowBattery.description</description>
<default>20</default>
<unitLabel>%</unitLabel>
</parameter>
<parameter name="updateInterval" type="integer" min="60" required="true" unit="s">
<label>@text/thing-type.config.shelly.updateInterval.label</label>
<description>@text/thing-type.config.shelly.updateInterval.description</description>
<default>900</default>
<unitLabel>seconds</unitLabel>
<advanced>true</advanced>
</parameter>
</config-description>
</config-description:config-descriptions>

View File

@ -17,9 +17,10 @@ message.config-status.error.missing-userid = No user ID in the Thing configurati
# Thing status descriptions
message.offline.conf-error-no-credentials = Device is password protected, but no credentials have been configured.
message.offline.conf-error-access-denied = Access denied, check user id and password.
message.offline.conf-error-wrong-mode = Device is no longer in the configured device mode {0}, required {1}. Delete the thing and re-discover the device.
message.offline.conf-error-wrong-mode = Device is in device mode {0}, thing requires {1}. Delete the thing and re-discover the device.
message.offline.status-error-connect = Unable to connect to device - {0}
message.offline.status-error-timeout = Device is not reachable (API timeout)
message.offline.status-error-unexpected-error = Unexpected error
message.offline.status-error-unexpected-error = Unexpected error: {0}
message.offline.status-error-unexpected-api-result = An unexpected API response. Please verify the logfile to get more detailed information.
message.offline.status-error-watchdog = Device is not responding, seems to be unavailable.
message.offline.status-error-restarted = The device has restarted and will be re-initialized.
@ -27,8 +28,8 @@ message.offline.status-error-fwupgrade = Firmware upgrade in progress
# General messages
message.versioncheck.failed = Unable to check firmware version: {0}
message.versioncheck.beta = Device is running a Beta version: {0}/{1}.
message.versioncheck.tooold = WARNING: Firmware might be too old, installed: {0}/{1}, required minimal {3}.
message.versioncheck.beta = Device is running a Beta version: {0}/{1}
message.versioncheck.tooold = WARNING: Firmware might be too old, installed: {0}/{1}, minimal required {2}
message.versioncheck.update = INFO: New firmware available: current version: {0}, new version: {1}
message.versioncheck.autocoiot = INFO: Firmware is full-filling the minimum version to auto-enable CoIoT
message.init.noipaddress = Unable to detect local IP address. Please make sure that IPv4 is enabled for this interface and check openHAB Network Configuration.
@ -76,7 +77,7 @@ thing-type.shelly.shellysmoke.description = Shelly Smoke Sensor (battery powered
thing-type.shelly.shellyflood.description = Shelly Flood Sensor (battery powered)
thing-type.shelly.shellysense.description = Shelly Sense (Motion Sensor and Remote IR Controller)
thing-type.shelly.shellybulb.description = Shelly Bulb (Dimmable RGB+W Bulb)
thing-type.shelly.shellybulbduo.description = Shelly Duo (Dimmable WW/CW Bulb)
thing-type.shelly.shellybulbduo.description = Shelly Duo (Dimmable RGB+W/CW Bulb)
thing-type.shelly.shellycolorbulb.description = Shelly Color Bulb (Dimmable RGB+W Bulb)
thing-type.shelly.shellyvintage.description = Shelly Vintage (Dimmable Vintage Bulb)
thing-type.shelly.shellyrgbw2-color.description = Shelly RGBW2 (LED Controller in Color Mode)
@ -85,6 +86,24 @@ thing-type.shelly.shellymotion.description = Shelly Motion (Motion Detection (ba
thing-type.shelly.shellygas.description = Shelly Gas (Gas Sensor - Gas Leak Detection)
thing-type.shelly.shellytrv.description = Shelly TRV (Radiator value, battery powered)
thing-type.shelly.shellyix3.description = Shelly ix3 (Activation Device with 3 inputs)
thing-type.shelly.shellypludht.description = Shelly Plus HT - Temperature and Humidity Sensor
# Plus/Pro devices
thing-type.shelly.shellyplus1.description = Shelly Plus 1 (Single Relay Switch)
thing-type.shelly.shellyplus1pm.description = Shelly Plus 1PM - Single Relay Switch with Power Meter
thing-type.shelly.shellyplus2-relay.description = Shelly Plus 2PM - Dual Relay Switch with Power Meter
thing-type.shelly.shellyplus2pm-roller.description = Shelly Plus 2PM - Roller Control with Power Meter
thing-type.shelly.shellyplusht.description = Shelly Plus HT - Humidity and Temperature sensor with display
thing-type.shelly.shellyplusi4.description = Shelly Plus i4 - 4xInput Device
thing-type.shelly.shellyplusi4dc.description = Shelly Plus i4DC - 4xDC Input Device
thing-type.shelly.shellypro1.description = Shelly Pro 1 - Single Relay Switch
thing-type.shelly.shellypro1pm.description = Shelly Pro 1PM - Single Relay Switch with Power Meter
thing-type.shelly.shellypro2-relay.description = Shelly Pro 2 - Dual Relay Switch
thing-type.shelly.shellypro2pm-relay.description= Shelly Pro 2PM - Dual Relay Switch with Power Meter
thing-type.shelly.shellypro2pm-roller.description = Shelly Pro 2PM - Roller Control with Power Meter
thing-type.shelly.shellypro3.description = Shelly Pro 3 - 3xRelay Switch
thing-type.shelly.shellypro4pm.description = Shelly Pro 4PM - 4xRelay Switch with Power Meter
# Plus/Pro devices
thing-type.shelly.shellyplus1.description = Shelly Plus 1 (Single Relay Switch)
@ -180,11 +199,11 @@ channel-group-type.shelly.relayChannel3.label = Relay 3
channel-group-type.shelly.relayChannel4.label = Relay 4
channel-group-type.shelly.dimmerChannel.label = Dimmer
channel-group-type.shelly.dimmerChannel.description = A Shelly Dimmer channel
channel-group-type.shelly.ix3Channel1.label = Input 1
channel-group-type.shelly.ix3Channel2.label = Input 2
channel-group-type.shelly.ix3Channel3.label = Input 3
channel-group-type.shelly.ix3Channel4.label = Input 4
channel-group-type.shelly.ix3Channel.description = Input Status
channel-group-type.shelly.iXChannel1.label = Input 1
channel-group-type.shelly.iXChannel2.label = Input 2
channel-group-type.shelly.iXChannel3.label = Input 3
channel-group-type.shelly.iXChannel4.label = Input 3
channel-group-type.shelly.iXChannel.description = Input Status
channel-group-type.shelly.rollerControl.label = Roller Control
channel-group-type.shelly.rollerControl.description = Controlling the roller mode
channel-group-type.shelly.meter.label = Power Meter
@ -200,9 +219,9 @@ channel-group-type.shelly.externalSensors.description = Temperatures from extern
channel-type.shelly.outputName.label = Output Name
channel-type.shelly.outputName.description = Output/Channel Name as configured in the Shelly App
channel-type.shelly.timerAutoOn.label = Auto-ON Timer
channel-type.shelly.timerAutoOn.description = When the output of the relay is switched off, it will be switched off automatically after n seconds
channel-type.shelly.timerAutoOn.description = When the output of the relay is switched off, it will be switched back on automatically after n seconds
channel-type.shelly.timerAutoOff.label = Auto-OFF Timer
channel-type.shelly.timerAutoOff.description = When the output of the relay is switched on, it will be switched on automatically after n seconds
channel-type.shelly.timerAutoOff.description = When the output of the relay is switched on, it will be switched back off automatically after n seconds
channel-type.shelly.timerActive.label = Auto ON/OFF timer active
channel-type.shelly.timerActive.description = ON: A timer is active, OFF: no timer active
channel-type.shelly.temperature.label = Temperature
@ -398,7 +417,7 @@ channel-type.shelly.boostTimer.label = Boost Timer
channel-type.shelly.boostTimer.description = Number of minutes to boost maximum power
channel-type.shelly.batVoltage.label = Battery Voltage
channel-type.shelly.batVoltage.description = Battery voltage (V)
channel-type.shelly.charger.label = Charger Connected
channel-type.shelly.charger.label = External Power
channel-type.shelly.charger.description = ON: External power is connected
channel-type.shelly.calibrated.label = Calibrated
channel-type.shelly.calibrated.description = ON: The device/sensor is calibrated
@ -429,7 +448,7 @@ channel-type.shelly.lastEvent.state.option.L = Long push
channel-type.shelly.lastEvent.state.option.SL = Short-Long push
channel-type.shelly.lastEvent.state.option.LS = Long-Short push
channel-type.shelly.eventCount.label = Event Count
channel-type.shelly.eventCount.description = Event Count
channel-type.shelly.eventCount.description = Number of input events received from device
channel-type.shelly.eventTrigger.label = Event
channel-type.shelly.eventTrigger.description = Event Trigger (ROLLER_OPEN/ROLLER_CLOSE/ROLLER_STOP)
channel-type.shelly.eventTrigger.option.ROLLER_OPEN = Roller is open
@ -506,6 +525,18 @@ message.manager.action.setwifirec-enable = The device performs an auto-restart i
message.manager.action.setwifirec-disable = WiFi Recovery Mode will be disabled.
message.manager.action.setwifirec-confirm = WiFi Recovery Mode has been {0}.
message.manager.action.setwifirec-failed = Unable to update setting for WiFi Recovery Mode: {0}
message.manager.action.setwifirangeext-enable = WiFi Range Extender will be enabled.
message.manager.action.setwifirangeext-disable = WiFi Range Extender will be disabled.
message.manager.action.setwifirangeext-confirm = WiFiRange Extender has been set, restart required: {0}
message.manager.action.setwifirangeext-failed = Unable to update setting for WiFi Range Extender: {0}
message.manager.action.setethernet-enable = Ethernet interface will be enabled, this requires a device reboot.
message.manager.action.setethernet-disable = Ethernet interface will be disabled, this may require a device reboot.
message.manager.action.setethernet-confirm = Ethernet interface has been {0}.
message.manager.action.setethernet-failed = Unable to update setting for Ethernet interface: {0}
message.manager.action.setbluetooth-enable = Bluetooth interface will be enabled, this requires a device reboot.
message.manager.action.setbluetooth-disable = Bluetooth interface will be disabled, this may require a device reboot.
message.manager.action.setbluetooth-confirm = Bluetooth interface has been {0}.
message.manager.action.setbluetooth-failed = Unable to update setting for Bluetooth interface: {0}
message.manager.action.aproaming-enable = WiFi Access Point Roaming will be enabled. Check product documentation for details.
message.manager.action.aproaming-disable = WiFi Access Point Roaming will be disabled.
message.manager.action.aproaming-confirm = Unable to update setting WiFi Access Point Roaming: {0}

View File

@ -5,7 +5,7 @@
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<thing-type id="shellybulb">
<label>Shelly Bulb (SHBLB-1)</label>
<label>Shelly Bulb</label>
<description>@text/thing-type.shelly.shellybulb.description</description>
<category>Lightbulb</category>
<channel-groups>
@ -20,7 +20,7 @@
</thing-type>
<thing-type id="shellybulbduo">
<label>Shelly Duo (SHBDUO-1)</label>
<label>Shelly Duo</label>
<description>@text/thing-type.shelly.shellybulbduo.description</description>
<category>Lightbulb</category>
<channel-groups>
@ -35,7 +35,7 @@
</thing-type>
<thing-type id="shellycolorbulb">
<label>Shelly Color Bulb (SHCB-1)</label>
<label>Shelly Color Bulb</label>
<description>@text/thing-type.shelly.shellycolorbulb.description</description>
<category>ColorLight</category>
<channel-groups>
@ -51,7 +51,7 @@
</thing-type>
<thing-type id="shellyvintage">
<label>Shelly Vintage (SHVIN-1)</label>
<label>Shelly Vintage</label>
<description>@text/thing-type.shelly.shellyvintage.description</description>
<category>Lightbulb</category>
<channel-groups>
@ -66,7 +66,7 @@
</thing-type>
<thing-type id="shellyrgbw2-color">
<label>Shelly RGBW2 Color Mode (SHRGBW2)</label>
<label>Shelly RGBW2 Color Mode</label>
<description>@text/thing-type.shelly.shellyrgbw2-color.description</description>
<category>ColorLight</category>
<channel-groups>
@ -81,7 +81,7 @@
</thing-type>
<thing-type id="shellyrgbw2-white">
<label>Shelly RGBW2 White Mode (SHRGBW2)</label>
<label>Shelly RGBW2 White Mode</label>
<description>@text/thing-type.shelly.shellyrgbw2-white.description</description>
<category>Lightbulb</category>
<channel-groups>

View File

@ -5,7 +5,7 @@
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<thing-type id="shelly1">
<label>Shelly 1 (SHSW-1)</label>
<label>Shelly 1</label>
<description>@text/thing-type.shelly.shelly1.description</description>
<channel-groups>
<channel-group id="relay" typeId="relayChannel"/>
@ -18,7 +18,7 @@
</thing-type>
<thing-type id="shelly1l">
<label>Shelly 1L (SHSW-L)</label>
<label>Shelly 1L</label>
<description>@text/thing-type.shelly.shelly1l.description</description>
<channel-groups>
<channel-group id="relay" typeId="relayChannel"/>
@ -32,7 +32,7 @@
</thing-type>
<thing-type id="shelly1pm">
<label>Shelly 1PM (SHSW-PM)</label>
<label>Shelly 1PM</label>
<description>@text/thing-type.shelly.shelly1pm.description</description>
<channel-groups>
<channel-group id="relay" typeId="relayChannel"/>
@ -46,7 +46,7 @@
</thing-type>
<thing-type id="shellyem">
<label>Shelly EM (SHEM)</label>
<label>Shelly EM</label>
<description>@text/thing-type.shelly.shellyem.description</description>
<channel-groups>
<channel-group id="meter1" typeId="meter">
@ -65,7 +65,7 @@
<!-- Shelly 3EM - device reports wrong service name: shellyem3, see README -->
<thing-type id="shellyem3">
<label>Shelly 3EM (SHEM-3)</label>
<label>Shelly EM3</label>
<description>@text/thing-type.shelly.shellyem3.description</description>
<channel-groups>
<channel-group id="meter1" typeId="meter">
@ -86,7 +86,7 @@
</thing-type>
<thing-type id="shelly2-relay">
<label>Shelly 2 Relay (SHSW-21)</label>
<label>Shelly 2 Relay</label>
<description>@text/thing-type.shelly.shelly2-relay.description</description>
<channel-groups>
@ -104,22 +104,8 @@
<config-description-ref uri="thing-type:shelly:relay"/>
</thing-type>
<thing-type id="shelly2-roller">
<label>Shelly 2 Roller (SHSW-21)</label>
<description>@text/thing-type.shelly.shelly2-roller.description</description>
<category>Rollershutter</category>
<channel-groups>
<channel-group id="roller" typeId="rollerControl"/>
<channel-group id="meter" typeId="meter"/>
<channel-group id="device" typeId="deviceStatus"/>
</channel-groups>
<representation-property>serviceName</representation-property>
<config-description-ref uri="thing-type:shelly:roller"/>
</thing-type>
<thing-type id="shelly25-relay">
<label>Shelly 2.5 Relay (SHSW-25)</label>
<label>Shelly 2.5 Relay</label>
<description>@text/thing-type.shelly.shelly25-relay.description</description>
<channel-groups>
<channel-group id="relay1" typeId="relayChannel">
@ -142,7 +128,7 @@
</thing-type>
<thing-type id="shelly25-roller">
<label>Shelly 2.5 Roller (SHSW-25)</label>
<label>Shelly 2.5 Roller</label>
<description>@text/thing-type.shelly.shelly25-roller.description</description>
<category>Rollershutter</category>
<channel-groups>
@ -156,7 +142,7 @@
</thing-type>
<thing-type id="shelly4pro">
<label>Shelly 4 Pro Relay (SHSW-44)</label>
<label>Shelly 4 Pro Relay</label>
<description>@text/thing-type.shelly.shelly4pro.description</description>
<channel-groups>
@ -192,7 +178,7 @@
</thing-type>
<thing-type id="shellyplug">
<label>Shelly Plug (SHPLG-1)</label>
<label>Shelly Plug</label>
<description>@text/thing-type.shelly.shellyplug.description</description>
<category>PowerOutlet</category>
<channel-groups>
@ -206,7 +192,7 @@
</thing-type>
<thing-type id="shellyplugs">
<label>Shelly Plug-S (SHPLG-S)</label>
<label>Shelly Plug-S</label>
<description>@text/thing-type.shelly.shellyplugs.description</description>
<category>PowerOutlet</category>
<channel-groups>
@ -220,7 +206,7 @@
</thing-type>
<thing-type id="shellyplugu1">
<label>Shelly Plug US (SHPLG-U1)</label>
<label>Shelly Plug US</label>
<description>@text/thing-type.shelly.shellyplugu1.description</description>
<category>PowerOutlet</category>
<channel-groups>
@ -234,7 +220,7 @@
</thing-type>
<thing-type id="shellyuni">
<label>Shelly UNI (SHUNI-1)</label>
<label>Shelly UNI</label>
<description>@text/thing-type.shelly.shellyuni.description</description>
<channel-groups>
@ -253,7 +239,7 @@
</thing-type>
<thing-type id="shellydimmer">
<label>Shelly Dimmer (SHDM-1)</label>
<label>Shelly Dimmer</label>
<description>@text/thing-type.shelly.shellydimmer.description</description>
<category>DimmableLight</category>
<channel-groups>
@ -267,7 +253,7 @@
</thing-type>
<thing-type id="shellydimmer2">
<label>Shelly Dimmer 2 (SHDM-2)</label>
<label>Shelly Dimmer 2</label>
<description>@text/thing-type.shelly.shellydimmer2.description</description>
<category>DimmableLight</category>
<channel-groups>
@ -281,17 +267,17 @@
</thing-type>
<thing-type id="shellyix3">
<label>Shelly ix3 (SHIX3-1)</label>
<label>Shelly ix3</label>
<description>@text/thing-type.shelly.shellyix3.description</description>
<channel-groups>
<channel-group id="status1" typeId="ix3Channel">
<label>@text/channel-group-type.shelly.ix3Channel1.label</label>
<channel-group id="status1" typeId="ixChannel">
<label>@text/channel-group-type.shelly.ixChannel1.label</label>
</channel-group>
<channel-group id="status2" typeId="ix3Channel">
<label>@text/channel-group-type.shelly.ix3Channel2.label</label>
<channel-group id="status2" typeId="ixChannel">
<label>@text/channel-group-type.shelly.ixChannel2.label</label>
</channel-group>
<channel-group id="status3" typeId="ix3Channel">
<label>@text/channel-group-type.shelly.ix3Channel3.label</label>
<channel-group id="status3" typeId="ixChannel">
<label>@text/channel-group-type.shelly.ixChannel3.label</label>
</channel-group>
<channel-group id="device" typeId="deviceStatus"/>
</channel-groups>
@ -315,9 +301,9 @@
<description>@text/channel-group-type.shelly.dimmerChannel.description</description>
</channel-group-type>
<channel-group-type id="ix3Channel">
<label>@text/channel-group-type.shelly.ix3Channel.label</label>
<description>@text/channel-group-type.shelly.ix3Channel.description</description>
<channel-group-type id="ixChannel">
<label>@text/channel-group-type.shelly.ixChannel.label</label>
<description>@text/channel-group-type.shelly.ixChannel.description</description>
</channel-group-type>
<channel-group-type id="rollerControl">
@ -576,7 +562,7 @@
<item-type>Number</item-type>
<label>@text/channel-type.shelly.meterPowerFactor.label</label>
<description>@text/channel-type.shelly.meterPowerFactor.description</description>
<state readOnly="true" pattern="%.3f %unit%">
<state readOnly="true" pattern="%.3f">
</state>
</channel-type>

View File

@ -5,7 +5,7 @@
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<thing-type id="shellyht">
<label>Shelly H&amp;T (SHHT-1)</label>
<label>Shelly H&amp;T</label>
<description>@text/thing-type.shelly.shellyht.description</description>
<category>Sensor</category>
<channel-groups>
@ -33,7 +33,7 @@
</thing-type>
<thing-type id="shellygas">
<label>Shelly GAS (SHGS-1)</label>
<label>Shelly Gas</label>
<description>@text/thing-type.shelly.shellygas.description</description>
<category>Sensor</category>
<channel-groups>
@ -46,7 +46,7 @@
</thing-type>
<thing-type id="shellyflood">
<label>Shelly Flood (SHWT-1)</label>
<label>Shelly Flood</label>
<description>@text/thing-type.shelly.shellyflood.description</description>
<category>Sensor</category>
<channel-groups>
@ -60,7 +60,7 @@
</thing-type>
<thing-type id="shellydw">
<label>Shelly Door/Window (SHDW-1)</label>
<label>Shelly Door/Window</label>
<description>@text/thing-type.shelly.shellydw.description</description>
<category>Sensor</category>
<channel-groups>
@ -74,7 +74,7 @@
</thing-type>
<thing-type id="shellydw2">
<label>Shelly Door/Window (SHDW-2)</label>
<label>Shelly Door/Window</label>
<description>@text/thing-type.shelly.shellydw2.description</description>
<category>Sensor</category>
<channel-groups>
@ -88,7 +88,7 @@
</thing-type>
<thing-type id="shellysense">
<label>Shelly Sense (SHSEN-1)</label>
<label>Shelly Sense</label>
<description>@text/thing-type.shelly.shellysense.description</description>
<channel-groups>
<channel-group id="control" typeId="control"/>
@ -102,7 +102,7 @@
</thing-type>
<thing-type id="shellybutton1">
<label>Shelly Button 1 (SHBTN-1)</label>
<label>Shelly Button 1</label>
<description>@text/thing-type.shelly.shellybutton1.description</description>
<category>WallSwitch</category>
<channel-groups>
@ -116,7 +116,7 @@
</thing-type>
<thing-type id="shellybutton2">
<label>Shelly Button 2 (SHBTN-2)</label>
<label>Shelly Button 2</label>
<description>@text/thing-type.shelly.shellybutton2.description</description>
<category>WallSwitch</category>
<channel-groups>
@ -130,7 +130,7 @@
</thing-type>
<thing-type id="shellymotion">
<label>Shelly Motion (SHMOS-01/SHMOS-02)</label>
<label>Shelly Motion</label>
<description>@text/thing-type.shelly.shellymotion.description</description>
<category>MotionDetector</category>
<channel-groups>
@ -144,7 +144,7 @@
</thing-type>
<thing-type id="shellytrv">
<label>Shelly TRV (SHTRV-01)</label>
<label>Shelly TRV</label>
<description>@text/thing-type.shelly.shellytrv.description</description>
<category>RadiatorControl</category>
<channel-groups>
@ -210,7 +210,6 @@
<state readOnly="false"></state>
</channel-type>
<channel-group-type id="sensorData">
<label>@text/channel-group-type.shelly.sensorData.label</label>
<description>@text/channel-group-type.shelly.sensorData.description</description>
@ -457,7 +456,7 @@
<item-type>Number:ElectricPotential</item-type>
<label>@text/channel-type.shelly.sensorADC.label</label>
<description>@text/channel-type.shelly.sensorADC.description</description>
<state readOnly="true" pattern="%.0f %unit%">
<state readOnly="true" pattern="%.3f %unit%">
</state>
</channel-type>

View File

@ -0,0 +1,259 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="shelly"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<thing-type id="shellyplus1">
<label>ShellyPlus 1</label>
<description>@text/thing-type.shelly.shellyplus1.description</description>
<channel-groups>
<channel-group id="relay" typeId="relayChannel"/>
<channel-group id="sensors" typeId="externalSensors"/>
<channel-group id="device" typeId="deviceStatus"/>
</channel-groups>
<representation-property>serviceName</representation-property>
<config-description-ref uri="thing-type:shelly:relay-gen2"/>
</thing-type>
<thing-type id="shellyplus1pm">
<label>ShellyPlus 1PM</label>
<description>@text/thing-type.shelly.shellyplus1pm.description</description>
<channel-groups>
<channel-group id="relay" typeId="relayChannel"/>
<channel-group id="meter" typeId="meter"/>
<channel-group id="sensors" typeId="externalSensors"/>
<channel-group id="device" typeId="deviceStatus"/>
</channel-groups>
<representation-property>serviceName</representation-property>
<config-description-ref uri="thing-type:shelly:relay-gen2"/>
</thing-type>
<thing-type id="shellyplus2pm-relay">
<label>ShellyPlus 2 Relay</label>
<description>@text/thing-type.shelly.shellyplus2-relay.description</description>
<channel-groups>
<channel-group id="relay1" typeId="relayChannel"/>
<channel-group id="meter1" typeId="meter"/>
<channel-group id="relay2" typeId="relayChannel"/>
<channel-group id="meter2" typeId="meter"/>
<channel-group id="device" typeId="deviceStatus"/>
</channel-groups>
<representation-property>serviceName</representation-property>
<config-description-ref uri="thing-type:shelly:relay-gen2"/>
</thing-type>
<thing-type id="shellyplus2pm-roller">
<label>ShellyPlus 2PM Roller</label>
<description>@text/thing-type.shelly.shellyplus2pm-roller.description</description>
<category>Rollershutter</category>
<channel-groups>
<channel-group id="roller" typeId="rollerControl"/>
<channel-group id="meter" typeId="meter"/>
<channel-group id="device" typeId="deviceStatus"/>
</channel-groups>
<representation-property>serviceName</representation-property>
<config-description-ref uri="thing-type:shelly:roller-gen2"/>
</thing-type>
<thing-type id="shellyplusi4">
<label>ShellyPlus i4</label>
<description>@text/thing-type.shelly.shellyplusi4.description</description>
<channel-groups>
<channel-group id="status1" typeId="ixChannel">
<label>@text/channel-group-type.shelly.ixChannel1.label</label>
</channel-group>
<channel-group id="status2" typeId="ixChannel">
<label>@text/channel-group-type.shelly.ixChannel2.label</label>
</channel-group>
<channel-group id="status3" typeId="ixChannel">
<label>@text/channel-group-type.shelly.ixChannel3.label</label>
</channel-group>
<channel-group id="status4" typeId="ixChannel">
<label>@text/channel-group-type.shelly.ixChannel4.label</label>
</channel-group>
<channel-group id="device" typeId="deviceStatus"/>
</channel-groups>
<representation-property>serviceName</representation-property>
<config-description-ref uri="thing-type:shelly:relay-gen2"/>
</thing-type>
<thing-type id="shellyplusi4dc">
<label>ShellyPlus i4DC</label>
<description>@text/thing-type.shelly.shellyplusi4dc.description</description>
<channel-groups>
<channel-group id="status1" typeId="ixChannel">
<label>@text/channel-group-type.shelly.ixChannel1.label</label>
</channel-group>
<channel-group id="status2" typeId="ixChannel">
<label>@text/channel-group-type.shelly.ixChannel2.label</label>
</channel-group>
<channel-group id="status3" typeId="ixChannel">
<label>@text/channel-group-type.shelly.ixChannel3.label</label>
</channel-group>
<channel-group id="status4" typeId="ixChannel">
<label>@text/channel-group-type.shelly.ixChannel4.label</label>
</channel-group>
<channel-group id="device" typeId="deviceStatus"/>
</channel-groups>
<representation-property>serviceName</representation-property>
<config-description-ref uri="thing-type:shelly:relay-gen2"/>
</thing-type>
<thing-type id="shellypro1">
<label>ShellyPro 1</label>
<description>@text/thing-type.shelly.shellypro1.description</description>
<channel-groups>
<channel-group id="relay" typeId="relayChannel"/>
<channel-group id="sensors" typeId="externalSensors"/>
<channel-group id="device" typeId="deviceStatus"/>
</channel-groups>
<representation-property>serviceName</representation-property>
<config-description-ref uri="thing-type:shelly:relay-gen2"/>
</thing-type>
<thing-type id="shellypro1pm">
<label>ShellyPro 1PM</label>
<description>@text/thing-type.shelly.shellypro1pm.description</description>
<channel-groups>
<channel-group id="relay" typeId="relayChannel"/>
<channel-group id="meter" typeId="meter"/>
<channel-group id="sensors" typeId="externalSensors"/>
<channel-group id="device" typeId="deviceStatus"/>
</channel-groups>
<representation-property>serviceName</representation-property>
<config-description-ref uri="thing-type:shelly:relay-gen2"/>
</thing-type>
<thing-type id="shellypro2-relay">
<label>ShellyPro 2 Relay</label>
<description>@text/thing-type.shelly.shellypro2-relay.description</description>
<channel-groups>
<channel-group id="relay1" typeId="relayChannel">
<label>@text/channel-group-type.shelly.relayChannel1.label</label>
</channel-group>
<channel-group id="meter1" typeId="meter">
<label>@text/channel-group-type.shelly.meter1.label</label>
</channel-group>
<channel-group id="relay2" typeId="relayChannel">
<label>@text/channel-group-type.shelly.relayChannel2.label</label>
</channel-group>
<channel-group id="meter2" typeId="meter">
<label>@text/channel-group-type.shelly.meter1.label</label>
</channel-group>
<channel-group id="device" typeId="deviceStatus"/>
</channel-groups>
<representation-property>serviceName</representation-property>
<config-description-ref uri="thing-type:shelly:relay-gen2"/>
</thing-type>
<thing-type id="shellypro2pm-relay">
<label>ShellyPro 2PM Relay</label>
<description>@text/thing-type.shelly.shellypro2pm-relay.description</description>
<channel-groups>
<channel-group id="relay1" typeId="relayChannel">
<label>@text/channel-group-type.shelly.relayChannel1.label</label>
</channel-group>
<channel-group id="meter1" typeId="meter">
<label>@text/channel-group-type.shelly.meter1.label</label>
</channel-group>
<channel-group id="relay2" typeId="relayChannel">
<label>@text/channel-group-type.shelly.relayChannel2.label</label>
</channel-group>
<channel-group id="meter2" typeId="meter">
<label>@text/channel-group-type.shelly.meter2.label</label>
</channel-group>
<channel-group id="device" typeId="deviceStatus"/>
</channel-groups>
<representation-property>serviceName</representation-property>
<config-description-ref uri="thing-type:shelly:relay-gen2"/>
</thing-type>
<thing-type id="shellypro2pm-roller">
<label>ShellyPro 2PM Roller</label>
<description>@text/thing-type.shelly.shellypro2pm-roller.description</description>
<category>Rollershutter</category>
<channel-groups>
<channel-group id="roller" typeId="rollerControl"/>
<channel-group id="meter" typeId="meter"/>
<channel-group id="device" typeId="deviceStatus"/>
</channel-groups>
<representation-property>serviceName</representation-property>
<config-description-ref uri="thing-type:shelly:roller-gen2"/>
</thing-type>
<thing-type id="shellypro3">
<label>ShellyPro 3</label>
<description>@text/thing-type.shelly.shellypro3.description</description>
<channel-groups>
<channel-group id="relay1" typeId="relayChannel">
<label>@text/channel-group-type.shelly.relayChannel1.label</label>
</channel-group>
<channel-group id="relay2" typeId="relayChannel">
<label>@text/channel-group-type.shelly.relayChannel2.label</label>
</channel-group>
<channel-group id="relay3" typeId="relayChannel">
<label>@text/channel-group-type.shelly.relayChannel3.label</label>
</channel-group>
<channel-group id="relay4" typeId="relayChannel">
<label>@text/channel-group-type.shelly.relayChannel4.label</label>
</channel-group>
<channel-group id="device" typeId="deviceStatus"/>
</channel-groups>
<representation-property>serviceName</representation-property>
<config-description-ref uri="thing-type:shelly:relay-gen2"/>
</thing-type>
<thing-type id="shellypro4pm">
<label>ShellyPro 4PM</label>
<description>@text/thing-type.shelly.shellypro4pm.description</description>
<channel-groups>
<channel-group id="relay1" typeId="relayChannel">
<label>@text/channel-group-type.shelly.relayChannel1.label</label>
</channel-group>
<channel-group id="meter1" typeId="meter">
<label>@text/channel-group-type.shelly.meter1.label</label>
</channel-group>
<channel-group id="relay2" typeId="relayChannel">
<label>@text/channel-group-type.shelly.relayChannel2.label</label>
</channel-group>
<channel-group id="meter2" typeId="meter">
<label>@text/channel-group-type.shelly.meter2.label</label>
</channel-group>
<channel-group id="relay3" typeId="relayChannel">
<label>@text/channel-group-type.shelly.relayChannel3.label</label>
</channel-group>
<channel-group id="meter3" typeId="meter">
<label>@text/channel-group-type.shelly.meter3.label</label>
</channel-group>
<channel-group id="relay4" typeId="relayChannel">
<label>@text/channel-group-type.shelly.relayChannel4.label</label>
</channel-group>
<channel-group id="meter4" typeId="meter">
<label>@text/channel-group-type.shelly.meter4.label</label>
</channel-group>
<channel-group id="device" typeId="deviceStatus"/>
</channel-groups>
<representation-property>serviceName</representation-property>
<config-description-ref uri="thing-type:shelly:relay-gen2"/>
</thing-type>
</thing:thing-descriptions>

View File

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="shelly"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<thing-type id="shellyplusht">
<label>ShellyPlus H&amp;T</label>
<description>@text/thing-type.shelly.shellypludht.description</description>
<category>Sensor</category>
<channel-groups>
<channel-group id="sensors" typeId="sensorData"/>
<channel-group id="battery" typeId="batteryStatus"/>
<channel-group id="device" typeId="deviceStatus"/>
</channel-groups>
<representation-property>serviceName</representation-property>
<config-description-ref uri="thing-type:shelly:battery-gen2"/>
</thing-type>
</thing:thing-descriptions>

View File

@ -101,6 +101,6 @@
<td align="right">${deviceRestarts}</td>
<td align="right">${timeoutErrors}</td>
<td align="right">${timeoutsRecovered}</td>
<td align="right" title="CoIoT Status: ${coiotStatus}">${coiotMessages}</td>
<td align="right">${coiotErrors}</td>
<td align="right" title="CoIOT Status: ${coiotStatus}">${protocolMessages}</td>
<td align="right">${protocolErrors}</td>
</tr>

View File

@ -55,6 +55,6 @@
<th>Device Restarts</th>
<th>Timeout Errors</th>
<th>Timeouts Recovered</th>
<th>CoIOT Messages</th>
<th>CoIOT Errors</th>
<th>Protocol Messages</th>
<th>Protocol Errors</th>
</tr>