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