From 66a11d6e4b5835bbce64a8dd37c659dc47affe95 Mon Sep 17 00:00:00 2001 From: Markus Michels Date: Sat, 18 Apr 2020 23:30:39 +0200 Subject: [PATCH] [shelly] Support for Duo, EM3, DW, Smoke, Addon; new CoAP-based updates; bug fixes (#6985) * Re-checkin based on latest PR review status Signed-off-by: Markus Michels --- bundles/org.openhab.binding.shelly/README.md | 312 ++++++-- .../internal/ShellyBindingConstants.java | 168 ++-- .../shelly/internal/ShellyHandlerFactory.java | 73 +- .../internal/api/ShellyApiException.java | 133 ++++ .../shelly/internal/api/ShellyApiJsonDTO.java | 429 ++++++---- .../shelly/internal/api/ShellyApiResult.java | 96 +++ .../internal/api/ShellyDeviceProfile.java | 213 ++--- .../internal/api/ShellyEventServlet.java | 89 +-- .../shelly/internal/api/ShellyHttpApi.java | 517 +++++++------ .../internal/coap/ShellyCoapHandler.java | 675 ++++++++++------ .../internal/coap/ShellyCoapJSonDTO.java | 25 +- .../internal/coap/ShellyCoapServer.java | 66 +- .../config/ShellyBindingConfiguration.java | 23 +- .../config/ShellyThingConfiguration.java | 5 +- .../discovery/ShellyDiscoveryParticipant.java | 147 ++-- .../discovery/ShellyThingCreator.java | 21 +- .../internal/handler/ShellyBaseHandler.java | 732 ++++++++++-------- .../handler/ShellyChannelDefinitionsDTO.java | 344 ++++++++ .../internal/handler/ShellyColorUtils.java | 66 +- .../internal/handler/ShellyComponents.java | 300 ++++--- .../handler/ShellyDeviceListener.java | 3 +- .../internal/handler/ShellyLightHandler.java | 497 ++++++------ .../handler/ShellyProtectedHandler.java | 11 +- .../internal/handler/ShellyRelayHandler.java | 255 +++--- .../internal/util/ShellyChannelCache.java | 135 ++++ .../util/ShellyTranslationProvider.java | 63 ++ .../internal/{ => util}/ShellyUtils.java | 70 +- .../internal/util/ShellyVersionDTO.java | 168 ++++ .../resources/ESH-INF/binding/binding.xml | 5 + .../main/resources/ESH-INF/config/config.xml | 154 +++- .../resources/ESH-INF/i18n/shelly.properties | 38 +- .../ESH-INF/i18n/shelly_de.properties | 215 +++-- .../main/resources/ESH-INF/thing/channels.xml | 367 --------- .../main/resources/ESH-INF/thing/device.xml | 59 +- .../main/resources/ESH-INF/thing/lights.xml | 150 +++- .../main/resources/ESH-INF/thing/relay.xml | 332 ++++++-- .../main/resources/ESH-INF/thing/sensor.xml | 193 ++++- 37 files changed, 4553 insertions(+), 2596 deletions(-) create mode 100644 bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api/ShellyApiException.java create mode 100644 bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api/ShellyApiResult.java create mode 100644 bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/handler/ShellyChannelDefinitionsDTO.java create mode 100644 bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/util/ShellyChannelCache.java create mode 100644 bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/util/ShellyTranslationProvider.java rename bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/{ => util}/ShellyUtils.java (70%) create mode 100644 bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/util/ShellyVersionDTO.java delete mode 100644 bundles/org.openhab.binding.shelly/src/main/resources/ESH-INF/thing/channels.xml diff --git a/bundles/org.openhab.binding.shelly/README.md b/bundles/org.openhab.binding.shelly/README.md index 5dc297d935950..ae8bf1d701361 100644 --- a/bundles/org.openhab.binding.shelly/README.md +++ b/bundles/org.openhab.binding.shelly/README.md @@ -4,30 +4,33 @@ This Binding integrated Shelly devices. ## Supported Devices -|Thing |Type | -|--------------------|--------------------------------------------------------| -| shelly1 | Shelly Single Relay Switch | -| shelly1pm | Shelly Single Relay Switch with integrated Power Meter | -| shellyem | Shelly EM with integrated Power Meter | -| shelly2-relay | Shelly Double Relay Switch in relay mode | -| shelly2-roller | Shelly2 in Roller Mode | -| shelly25-relay | Shelly 2.5 in Relay Switch | -| shelly25-roller | Shelly 2.5 in Roller Mode | -| shelly4pro | Shelly 4x Relay Switch | -| shellydimmer | Shelly Dimmer | -| shellyplugs | Shelly Plug-S | -| shellyplug | Shelly Plug | -| shellyrgbw2 | Shelly RGB Controller | -| shellybulb | Shelly Bulb in Color or White Mode | -| shellyht | Shelly Sensor (temp+humidity) | -| shellyflood | Shelly Flood Sensor | -| shellysmoke | Shelly Smoke Sensor | -| shellysense | Shelly Motion and IR Controller | -| shellydevice | A password protected Shelly device or an unknown type | +| Thing Type | Model | Vendor ID | +|--------------------|--------------------------------------------------------|-----------| +| shelly1 | Shelly Single Relay Switch | SHSW-1 | +| shelly1pm | Shelly Single Relay Switch with integrated Power Meter | SHSW-PM | +| shelly2-relay | Shelly Double Relay Switch in relay mode | SHSW-21 | +| shelly2-roller | Shelly2 in Roller Mode | SHSW-21 | +| shelly25-relay | Shelly 2.5 in Relay Switch | SHSW-25 | +| shelly25-roller | Shelly 2.5 in Roller Mode | SHSW-25 | +| shelly4pro | Shelly 4x Relay Switch | SHSW-44 | +| shellydimmer | Shelly Dimmer | SHDM-1 | +| shellyplug | Shelly Plug | SHPLG2-1 | +| shellyplugs | Shelly Plug-S | SHPLG-S | +| shellyem | Shelly EM with integrated Power Meters | SHEM | +| shellyem3 | Shelly EM3 with 3 integrated Power Meter | SHEM-3 | +| shellyrgbw2 | Shelly RGB Controller | SHRGBW2 | +| shellybulb | Shelly Bulb in Color or White Mode | SHBLB-1 | +| shellybulbduo | Shelly Duo (White Mode) | SHBDUO-1 | +| shellyht | Shelly Sensor (temp+humidity) | SHHT-1 | +| shellyflood | Shelly Flood Sensor | SHWT-1 | +| shellysmoke | Shelly Smoke Sensor | | +| shellydw | Shelly Door/Window | SHDW-1 | +| shellysense | Shelly Motion and IR Controller | SHSEN-1 | +| shellydevice | A password protected Shelly device or an unknown type | | ## Firmware -To utilize all features the binding requires firmware version 1.5.2 or newer. +To utilize all features the binding requires firmware version 1.5.7 or newer, version 1.6 is strongly recommended. This should be available for all devices. Older versions work in general, but have impacts to functionality (e.g. no events for battery powered devices). @@ -35,20 +38,53 @@ The binding displays a WARNING in the log if the firmware is older. It also informs you when an update is available. Use the device' web ui or the Shelly App to perform the update. +## Other resources + +Check the following resources for additional information + + ## Discovery The binding uses mDNS to discover the Shelly devices. They periodically announce their presence, which is used by the binding to find them on the local network. -Make sure to wake-up battery powered devices (press the button inside the device), so that they show up on the network. Sometimes you need to run the manual discovery multiple times until you see all your devices. +### Dynamic creation of channels + +The Shelly series of devices has many combinations of relays, meters (different versions), sensors etc. For this the binding creates various channels dynamically based on the status information provided by the device at initialization time. +If a channel is missing make sure the thing was discovered correctly and is ONLINE. If a channel is missing delete the thing and re-discover it. + +### Important for battery power devices + +Make sure to wake up battery powered devices (press the button inside the device), so that they show up on the network. +The device has a push button inside, open the case, press that button and the LED starts blinking. +The device should show up in the Inbox and can be added. + +` +Important: If device is in sleep mode and can't be reached by the binding, the Thing will change into UNKNOWN state. +Once the device wakes up, the thing will perform initialization and the state will change to ONLINE. +` + +The first time a device is discovered and initialized successfully, the binding will be able to perform auto-initialization when OH is restarted. +Waking up the device triggers the event URL and/or CoIoT packet, which is processed by the binding and triggers initialization. +Once a device is initialized, it is no longer necessary to manually wake it up after an openHAB restart. + +Devices that have no battery are expected to be ON/reachable on the network at all times. +Otherwise the thing will go OFFLINE with COMMUNICATION_ERROR as status. + +### Re-discover when IP address has changed + Important: The IP address should not be changed after the device is added to openHAB. + This can be achieved by - assigning a static IP address or - using DHCP and setup the router to always assign the same IP address to the device +When the IP address changes for a device you need to delete the Thing and then re-discover the device. +In this case channel linkage gets lost and you need to re-link the channels/items. + ### Password Protected Devices The Shelly devices can be configured to require authentication through a user id and password. @@ -90,27 +126,46 @@ The binding has the following configuration options: Every device has a channel group `device` with the following channels: -|Group |Channel |Type |read-only|Desciption | -|----------|-------------|---------|---------|---------------------------------------------------------------------------------| -|device |uptime |Number |yes |Number of seconds since the device was powered up | -| |wifiSignal |Number |yes |WiFi signal strength (4=excellent, 3=good, 2=not string, 1=unreliable, 0=none) | -| |alarm |Trigger |yes |Most recent alarm for health check | +|Group |Channel |Type |read-only|Desciption | +|----------|-------------------|--------|---------|---------------------------------------------------------------------------------| +|device |uptime |Number |yes |Number of seconds since the device was powered up | +| |wifiSignal |Number |yes |WiFi signal strength (4=excellent, 3=good, 2=not string, 1=unreliable, 0=none) | +| |innerTemp |Number |yes |Internal device temperature (when provided by the device) | +| |wakeupReason |String |yes |Sensors only: Last wake-up reason (POWERON/PERIODIC/BUTTON/BATTERY/ALARM) | +| |alarm |Trigger |yes |Most recent alarm for health check | +| |accumulatedWatts |Number |yes |Accumulated power in W of the device (including all meters) | +| |accumulatedTotal |Number |yes |Accumulated total power in kw/h of the device (including all meters) | +| |accumulatedReturned|Number |yes |Accumulated returned power in kw/h of the device (including all meters) | +The accumulated channels are only available for devices with more than 1 meter. accumulatedReturned only for the EM and EM3. -### Alarm Events +### Events / Alarms The binding provides health monitoring functions for the device. When an alarm condition is detected the channel alarm gets triggered and provides one of the following alarm types: +### Non-battery powerd devices + |Event Type|Description| |------------|-----------------------------------------------------------------------------------------------------------------| -|WEAK_SIGNAL |An alarm is triggered when RSSI is < -80, which indicates an unstable connection. | |RESTARTED |The device has been restarted. This could be an indicator for a firmware problem. | +|WEAK_SIGNAL |An alarm is triggered when RSSI is < -80, which indicates an unstable connection. | |OVER_TEMP |The device is over heating, check installation and housing. | |OVER_LOAD |An over load condition has been detected, e.g. from the roller motor. | |OVER_POWER |Maximum allowed power was exceeded. The relay was turned off. | -|LOAD_ERROR |Device reported a load problem. | -|LOW_BATTERY |Device reported a load problem. | +|LOAD_ERROR |Device reported a load problem, so far Dimmer only. | + +### Sensors + +|Event Type|Description| +|------------|-----------------------------------------------------------------------------------------------------------------| +|POWERON |Device was powered on. | +|PERIODIC |Periodic wakeup. | +|BUTTON |Button was pressed, e.g. to wakeup the device. | +|SENSOR |Wake-up due to updated sensor data. | +|ALARM |Alarm condition was detected, check status, could be OPENED for the DW, flood alarm, smoke alarm | +|BATTERY |Device reported an update to the battery status. | + A new alarm will be triggered on a new condition or every 5 minutes if the condition persists. @@ -129,9 +184,10 @@ end |----------|-------------|---------|---------|---------------------------------------------------------------------------------| |relay |output |Switch |r/w |Controls the relay's output channel (on/off) | | |input |Switch |yes |ON: Input/Button is powered, see General Notes on Channels | -|sensors |temperature1 |Number |yes |Temperature value of external sensor #1 (if connected to device) | -| |temperature2 |Number |yes |Temperature value of external sensor #2 (if connected to device) | -| |temperature3 |Number |yes |Temperature value of external sensor #3 (if connected to device) | +|sensors |temperature1 |Number |yes |Temperature value of external sensor #1 (if connected to temp/hum addon) | +| |temperature2 |Number |yes |Temperature value of external sensor #2 (if connected to temp/hum addon) | +| |temperature3 |Number |yes |Temperature value of external sensor #3 (if connected to temp/hum addon) | +| |humidity |Number |yes |Humidity in percent (if connected to temp/hum addon) | ### Shelly 1PM (thing-type: shelly1pm) @@ -144,11 +200,12 @@ end | |lastPower1 |Number |yes |Energy consumption in Watts for a round minute, 1 minute ago | | |lastPower2 |Number |yes |Energy consumption in Watts for a round minute, 2 minutes ago | | |lastPower3 |Number |yes |Energy consumption in Watts for a round minute, 3 minutes ago | -| |totalKWH |Number |yes |Total energy consumption in Watts since the device powered up (reset on restart) | +| |totalKWH |Number |yes |Total energy consumption in Watts since the device powered up (resets on restart)| | |timestamp |String |yes |Timestamp of the last measurement | -|sensors |temperature1 |Number |yes |Temperature value of external sensor #1 (if connected to device) | -| |temperature2 |Number |yes |Temperature value of external sensor #2 (if connected to device) | -| |temperature3 |Number |yes |Temperature value of external sensor #3 (if connected to device) | +|sensors |temperature1 |Number |yes |Temperature value of external sensor #1 (if connected to temp/hum addon) | +| |temperature2 |Number |yes |Temperature value of external sensor #2 (if connected to temp/hum addon) | +| |temperature3 |Number |yes |Temperature value of external sensor #3 (if connected to temp/hum addon) | +| |humidity |Number |yes |Humidity in percent (if connected to temp/hum addon) | ### Shelly EM (thing-type: shellyem) @@ -158,18 +215,51 @@ end | |input |Switch |yes |ON: Input/Button is powered, see General Notes on Channels | | |button |Trigger |yes |Event trigger with payload SHORT_PRESSED or LONG_PRESSED (FW 1.5.6+) | |meter1 |currentWatts |Number |yes |Current power consumption in Watts | -| |totalKWH |Number |yes |Total energy consumption in Watts since the device powered up (reset on restart) | +| |totalKWH |Number |yes |Total energy consumption in Watts since the device powered up (resets on restart)| | |returnedKWH |Number |yes |Total returned energy, kw/h | | |reactiveWatts|Number |yes |Instantaneous reactive power, Watts | | |voltage |Number |yes |RMS voltage, Volts | | |timestamp |String |yes |Timestamp of the last measurement | |meter2 |currentWatts |Number |yes |Current power consumption in Watts | -| |totalKWH |Number |yes |Total energy consumption in Watts since the device powered up (reset on restart) | +| |totalKWH |Number |yes |Total energy consumption in Watts since the device powered up (resets on restart)| | |returnedKWH |Number |yes |Total returned energy, kw/h | | |reactiveWatts|Number |yes |Instantaneous reactive power, Watts | | |voltage |Number |yes |RMS voltage, Volts | | |timestamp |String |yes |Timestamp of the last measurement | +### Shelly EM3 (thing-type: shellyem3) + +|Group |Channel |Type |read-only|Desciption | +|----------|-------------|---------|---------|---------------------------------------------------------------------------------| +|relay |output |Switch |r/w |Controls the relay's output channel (on/off) | +| |input |Switch |yes |ON: Input/Button is powered, see General Notes on Channels | +| |button |Trigger |yes |Event trigger with payload SHORT_PRESSED or LONG_PRESSED (FW 1.5.6+) | +|meter1 |currentWatts |Number |yes |Current power consumption in Watts | +| |totalKWH |Number |yes |Total energy consumption in Watts since the device powered up (resets on restart)| +| |returnedKWH |Number |yes |Total returned energy, kw/h | +| |reactiveWatts|Number |yes |Instantaneous reactive power, Watts | +| |voltage |Number |yes |RMS voltage, Volts | +| |current |Number |yes |Current in A | +| |powerFactor |Number |yes |Power Factor | +| |timestamp |String |yes |Timestamp of the last measurement | +|meter2 |currentWatts |Number |yes |Current power consumption in Watts | +| |totalKWH |Number |yes |Total energy consumption in Watts since the device powered up (resets on restart)| +| |returnedKWH |Number |yes |Total returned energy, kw/h | +| |reactiveWatts|Number |yes |Instantaneous reactive power, Watts | +| |voltage |Number |yes |RMS voltage, Volts | +| |current |Number |yes |Current in A | +| |powerFactor |Number |yes |Power Factor | +| |timestamp |String |yes |Timestamp of the last measurement | +|meter3 |currentWatts |Number |yes |Current power consumption in Watts | +| |totalKWH |Number |yes |Total energy consumption in Watts since the device powered up (resets on restart)| +| |returnedKWH |Number |yes |Total returned energy, kw/h | +| |reactiveWatts|Number |yes |Instantaneous reactive power, Watts | +| |voltage |Number |yes |RMS voltage, Volts | +| |current |Number |yes |Current in A | +| |powerFactor |Number |yes |Power Factor | +| |timestamp |String |yes |Timestamp of the last measurement | + + ### Shelly 2 - relay mode thing-type: shelly2-relay) |Group |Channel |Type |read-only|Description | @@ -190,7 +280,7 @@ end | |lastPower1 |Number |yes |Energy consumption in Watts for a round minute, 1 minute ago | | |lastPower2 |Number |yes |Energy consumption in Watts for a round minute, 2 minutes ago | | |lastPower3 |Number |yes |Energy consumption in Watts for a round minute, 3 minutes ago | -| |totalKWH |Number |yes |Total energy consumption in Watts since the device powered up (reset on restart) | +| |totalKWH |Number |yes |Total energy consumption in Watts since the device powered up (resets on restart)| | |timestamp |String |yes |Timestamp of the last measurement | ### Shelly 2 - roller mode thing-type: shelly2-roller) @@ -210,6 +300,8 @@ end | |totalKWH |Number |yes |Total energy consumption in Watts since the device powered up (reset on restart) | | |timestamp |String |yes |Timestamp of the last measurement | +The roller positioning calibration has to be performed using the Shelly App before the position can be set in percent. + ### Shelly 2.5 - relay mode (thing-type:shelly25-relay) The Shelly 2.5 includes 2 meters, one for each channel. @@ -227,17 +319,17 @@ The Shelly 2.5 includes 2 meters, one for each channel. However, it doesn't make sense to differ power consumption for the roller moving up vs. moving down. For this the binding aggregates the power consumption of both relays and includes the values in "meter1". -|Group |Channel |Type |read-only|Description | -|----------|-------------|---------|---------|-------------------------------------------------------------------------------------------| -|roller |control |Rollershutter |r/w |can be open (0%), stop, or close (100%); could also handle ON (open) and OFF (close)| -| |rollerpos |Dimmer |r/w |Roller position: 100%=open...0%=closed; gets updated when the roller stopped | -| |input |Switch |yes |ON: Input/Button is powered, see General Notes on Channels | -| |lastDirection|String |yes |Last direction: open or close | -| |stopReason |String |yes |Last stop reasons: normal, safety_switch or obstacle | -| |calibrating |Switch |yes |ON: Roller is in calibration mode, OFF: normal mode (no calibration) | -| |positioning |Switch |yes |ON: Roller is positioning/moving | -| |event |Trigger |yes |Roller event/trigger with payload ROLLER_OPEN / ROLLER_CLOSE / ROLLER_STOP | -|meter | | | |See group meter1 for Shelly 2 | +|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 | +| |lastDirection|String |yes |Last direction: open or close | +| |stopReason |String |yes |Last stop reasons: normal, safety_switch or obstacle | +| |event |Trigger |yes |Roller event/trigger with payload ROLLER_OPEN / ROLLER_CLOSE / ROLLER_STOP | +|meter | | | |See group meter1 for Shelly 2 | + +The roller positioning calibration has to be performed using the Shelly App before the position can be set in percent. ### Shelly4 Pro (thing-type: shelly4pro) @@ -280,9 +372,11 @@ The Shelly 4Pro provides 4 relays and 4 power meters. | |lastPower1 |Number |yes |Energy consumption in Watts for a round minute, 1 minute ago | | |lastPower2 |Number |yes |Energy consumption in Watts for a round minute, 2 minutes ago | | |lastPower3 |Number |yes |Energy consumption in Watts for a round minute, 3 minutes ago | -| |totalKWH |Number |yes |Total energy consumption in Watts since the device powered up (reset on restart) | +| |totalKWH |Number |yes |Total energy consumption in Watts since the device powered up (resets on restart)| | |timestamp |String |yes |Timestamp of the last measurement | +The Dimmer should be calibrated using the Shelly App. + ### Shelly Bulb (thing-type: shellybulb) |Group |Channel |Type |read-only|Description | @@ -308,11 +402,30 @@ The Shelly 4Pro provides 4 relays and 4 power meters. | |temperature |Number |r/w |color temperature (K): 0..100% or 3000..6500 | | |brightness |Dimmer | |Brightness: 0..100% or 0..100 | -### Shelly RGBW2 in Color Mode (thing-type: shellyrgbw2-color) +#### Shelly Duo (thing-type: shellybulbduo) + +|Group |Channel |Type |read-only|Description | +|----------|-------------|---------|---------|-----------------------------------------------------------------------| +|control |power |Switch |r/w |Switch light ON/OFF | +| |autoOn |Number |r/w |Sets a timer to turn the device ON after every OFF; in sec | +| |autoOff |Number |r/w |Sets a timer to turn the device OFF after every ON: in sec | +| |timerActive |Switch |yes |ON: An auto-on/off timer is active | +|white | | | |Color settings: only valid in WHITE mode | +| |temperature |Number |r/w |color temperature (K): 0..100% or 2700..6500 | +| |brightness |Dimmer | |Brightness: 0..100% or 0..100 | +|meter |currentWatts |Number |yes |Current power consumption in Watts | +| |lastPower1 |Number |yes |Energy consumption in Watts for a round minute, 1 minute ago | +| |lastPower2 |Number |yes |Energy consumption in Watts for a round minute, 2 minutes ago | +| |lastPower3 |Number |yes |Energy consumption in Watts for a round minute, 3 minutes ago | +| |totalKWH |Number |yes |Total energy consumption in Watts since the device powered up (resets on restart)| +| |timestamp |String |yes |Timestamp of the last measurement | + + ## Shelly RGBW2 in Color Mode (thing-type: shellyrgbw2-color) |Group |Channel |Type |read-only|Desciption | |----------|-------------|---------|---------|-----------------------------------------------------------------------| |control |power |Switch |r/w |Switch light ON/OFF | +| |button |Trigger |yes |Event trigger with payload SHORT_PRESSED or LONG_PRESSED (FW 1.5.6+) | | |autoOn |Number |r/w |Sets a timer to turn the device ON after every OFF command; in seconds| | |autoOff |Number |r/w |Sets a timer to turn the device OFF after every ON command; in seconds| | |timerActive |Switch |yes |ON: An auto-on/off timer is active | @@ -332,34 +445,51 @@ The Shelly 4Pro provides 4 relays and 4 power meters. |Group |Channel |Type |read-only|Desciption | |----------|-------------|---------|---------|-----------------------------------------------------------------------| -|control |autoOn |Number |r/w |Sets a timer to turn the device ON after every OFF command; in seconds| +|control |input |Switch |yes |State of Input | +|channel1 |brightness |Dimmer |r/w |Channel 1: Brightness: 0..100, control power state with ON/OFF | +| |button |Trigger |yes |Event trigger with payload SHORT_PRESSED or LONG_PRESSED (FW 1.5.6+) | +| |autoOn |Number |r/w |Sets a timer to turn the device ON after every OFF command; in seconds| +| |autoOff |Number |r/w |Sets a timer to turn the device OFF after every ON command; in seconds| +| |timerActive |Switch |yes |ON: An auto-on/off timer is active | +|channel2 |brightness |Dimmer |r/w |Channel 2: Brightness: 0..100, control power state with ON/OFF | +| |button |Trigger |yes |Event trigger with payload SHORT_PRESSED or LONG_PRESSED (FW 1.5.6+) | +| |autoOn |Number |r/w |Sets a timer to turn the device ON after every OFF command; in seconds| +| |autoOff |Number |r/w |Sets a timer to turn the device OFF after every ON command; in seconds| +| |timerActive |Switch |yes |ON: An auto-on/off timer is active | +|channel3 |brightness |Dimmer |r/w |Channel 3: Brightness: 0..100, control power state with ON/OFF | +| |button |Trigger |yes |Event trigger with payload SHORT_PRESSED or LONG_PRESSED (FW 1.5.6+) | +| |autoOn |Number |r/w |Sets a timer to turn the device ON after every OFF command; in seconds| +| |autoOff |Number |r/w |Sets a timer to turn the device OFF after every ON command; in seconds| +| |timerActive |Switch |yes |ON: An auto-on/off timer is active | +|channel4 |brightness |Dimmer |r/w |Channel 5: Brightness: 0..100, control power state with ON/OFF | +| |button |Trigger |yes |Event trigger with payload SHORT_PRESSED or LONG_PRESSED (FW 1.5.6+) | +| |autoOn |Number |r/w |Sets a timer to turn the device ON after every OFF command; in seconds| | |autoOff |Number |r/w |Sets a timer to turn the device OFF after every ON command; in seconds| | |timerActive |Switch |yes |ON: An auto-on/off timer is active | -|channel1 |power |Switch |r/w |Channel 1: Turn channel on/off | -| |brightness |Dimmer |r/w |Channel 1: Brightness: 0..100 | -|channel2 |power |Switch |r/w |Channel 2: Turn channel on/off | -| |brightness |Dimmer |r/w |Channel 2: Brightness: 0..100 | -|channel3 |power |Switch |r/w |Channel 3: Turn channel on/off | -| |brightness |Dimmer |r/w |Channel 3: Brightness: 0..100 | -|channel4 |power |Switch |r/w |Channel 4: Turn channel on/off | -| |brightness |Dimmer |r/w |Channel 4: Brightness: 0..100 | -|meter1 |currentWatts |Number |yes |Channel 1: Current power consumption in Watts | -|meter2 |currentWatts |Number |yes |Channel 2: Current power consumption in Watts | -|meter3 |currentWatts |Number |yes |Channel 3: Current power consumption in Watts | -|meter4 |currentWatts |Number |yes |Channel 4: Current power consumption in Watts | +|meter |currentWatts |Number |yes |Current power consumption in Watts (all channels) | Please note that the settings of channel group color are only valid in color mode and vice versa for white mode. -The current firmware doesn't support the timestamp report for the meters. -In this case "n/a" is returned. -Maybe an upcoming firmware release adds this attribute, then the correct value is returned; +The current firmware doesn't support the timestamp report for the meters. +The binding emulates this by using the system time on every update. + +In white mode each RGBW2 channel is defined as DimmableLight. +This means that the brightness channel has 2 functions +- Sending ON/OFF (OnOffType) to power on/off the channel +- Sending a Number to set the brightness (percentage 0..100) + +Sending brightness 0 will automatically turn off the channel if it's currently on. +Sending brightness > 0 will automatically turn on the channel if it's currently off. +You can define 2 items (1 Switch, 1 Number) mapping to the same channel, see example rules. -### Shelly HT (thing-type: shellyht) +### Shelly H&T (thing-type: shellyht) |Group |Channel |Type |read-only|Description | |----------|-------------|---------|---------|-----------------------------------------------------------------------| |sensors |temperature |Number |yes |Temperature, unit is reported by tempUnit | | |humidity |Number |yes |Relative humidity in % | -| |last_update |String |yes |Timestamp of the last update (values read by the binding) | +| |charger |Number |yes |ON: USB charging cable is | +| |wakeupReason |String |yes |Last reason for a device wake-up (battery, button, periodic, poweron, sensor or alarm) | +| |lastUpdate |DateTime |yes |Timestamp of the last update (any sensor value changed) | |battery |batteryLevel |Number |yes |Battery Level in % | | |voltage |Number |yes |Voltage of the battery | | |lowBattery |Switch |yes |Low battery alert (< 20%) | @@ -370,7 +500,39 @@ Maybe an upcoming firmware release adds this attribute, then the correct value i |----------|-------------|---------|---------|-----------------------------------------------------------------------| |sensors |temperature |Number |yes |Temperature, unit is reported by tempUnit | | |flood |Switch |yes |ON: Flooding condition detected, OFF: no flooding | -| |last_update |String |yes |Timestamp of the last update (values read by the binding) | +| |wakeupReason |String |yes |Last reason for a device wake-up (battery, button, periodic, poweron, sensor or alarm) | +| |lastUpdate |DateTime |yes |Timestamp of the last update (any sensor value changed) | +|battery |batteryLevel |Number |yes |Battery Level in % | +| |voltage |Number |yes |Voltage of the battery | +| |lowBattery |Switch |yes |Low battery alert (< 20%) | + +### Shelly Door/Window (thing type: shellydw) + +|Group |Channel |Type |read-only|Description | +|----------|-------------|---------|---------|-----------------------------------------------------------------------| +|sensors |state |Contact |yes |OPEN: Contact is open, CLOSED: Contact is closed | +| |lux |Number |yes |Brightness in Lux | +| |illumination |String |yes |Current illumination: dark/twilight/bright | +| |titl |Number |yes |Tilt in ° (angle), -1 indicates that the sensor is not calibrated | +| |vibration |Switch |yes |ON: Vibration detected | +| |wakeupReason |String |yes |Last reason for a device wake-up (battery, button, periodic, poweron, sensor or alarm) | +| |lastUpdate |DateTime |yes |Timestamp of the last update (any sensor value changed) | +| |lastError |String |yes |Last device error. | +|battery |batteryLevel |Number |yes |Battery Level in % | +| |voltage |Number |yes |Voltage of the battery | +| |lowBattery |Switch |yes |Low battery alert (< 20%) | + +You should calibrate the sensor using the Shelly App to get information on the tilt status. + +### Shelly Smoke(thing type: shellysmoke) + +|Group |Channel |Type |read-only|Description | +|----------|-------------|---------|---------|-----------------------------------------------------------------------| +|sensors |temperature |Number |yes |Temperature, unit is reported by tempUnit | +| |smoke |Number |yes |ON: Smoke detected | +| |wakeupReason |String |yes |Last reason for a device wake-up (battery, button, periodic, poweron, sensor or alarm) | +| |lastUpdate |DateTime |yes |Timestamp of the last update (any sensor value changed) | +| |lastError |String |yes |Last device error. | |battery |batteryLevel |Number |yes |Battery Level in % | | |voltage |Number |yes |Voltage of the battery | | |lowBattery |Switch |yes |Low battery alert (< 20%) | @@ -390,7 +552,7 @@ Maybe an upcoming firmware release adds this attribute, then the correct value i | |humidity |Number |yes |Relative humidity in % | | |lux |Number |yes |Brightness in Lux | | |motion |Switch |yes |ON: Motion detected, OFF: No motion (check also motionTimer) | -| |last_update |String |yes |Timestamp of the last update (values read by the binding) | +| |lastUpdate |DateTime |yes |Timestamp of the last update (any sensor value changed) | |battery |batteryLevel |Number |yes |Battery Level in % | | |batteryAlert |Switch |yes |Low battery alert | @@ -421,8 +583,6 @@ Thing shelly:shellyflood:XXXXXX "ShellyFlood" @ "cellar" [ deviceIp="10.0.0.103" ``` /* Relays */ Switch Shelly_XXXXX3_Relay "Garage Light" {channel="shelly:shelly1:XXXXX3:relay#output"} -Switch Shelly_XXXXX3_OverPower "Garage Light Over Power" {channel="shelly:shelly1:XXXXX3:relay#overpower"} -Switch Shelly_XXXXX3_OverTemp "Garage Light Over Temperature" {channel="shelly:shelly1:XXXXX3:relay#overtemperature"} Number Shelly_XXXXX3_AutoOnTimer "Garage Light Auto On Timer" {channel="shelly:shelly1:XXXXX3:relay#autoOn"} Number Shelly_XXXXX3_AutoOffTimer "Garage Light Auto Off Timer" {channel="shelly:shelly1:BA2F18:relay#autoOff"} Switch Shelly_XXXXX3_Relay "Garage Light" {channel="shelly:shelly1:XXXXX3:relay#output"} 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 205ee9b786746..0b26ea2e2afe8 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 @@ -37,6 +37,7 @@ public class ShellyBindingConstants { public static final String THING_TYPE_SHELLY1_STR = "shelly1"; public static final String THING_TYPE_SHELLY1PN_STR = "shelly1pm"; public static final String THING_TYPE_SHELLYEM_STR = "shellyem"; + public static final String THING_TYPE_SHELLYEM3_STR = "shellyem3"; 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"; @@ -48,24 +49,33 @@ public class ShellyBindingConstants { public static final String THING_TYPE_SHELLYPLUGS_STR = "shellyplugs"; public static final String THING_TYPE_SHELLYDIMMER_STR = "shellydimmer"; public static final String THING_TYPE_SHELLYBULB_STR = "shellybulb"; + public static final String THING_TYPE_SHELLYDUO_STR = "shellybulbduo"; + public static final String THING_TYPE_SHELLYVINTAGE_STR = "shellybulbvintage"; public static final String THING_TYPE_SHELLYRGBW2_PREFIX = "shellyrgbw2"; public static final String THING_TYPE_SHELLYRGBW2_COLOR_STR = "shellyrgbw2-color"; public static final String THING_TYPE_SHELLYRGBW2_WHITE_STR = "shellyrgbw2-white"; public static final String THING_TYPE_SHELLYHT_STR = "shellyht"; public static final String THING_TYPE_SHELLYSMOKE_STR = "shellysmoke"; public static final String THING_TYPE_SHELLYFLOOD_STR = "shellyflood"; + public static final String THING_TYPE_SHELLYDOORWIN_STR = "shellydw"; public static final String THING_TYPE_SHELLYEYE_STR = "shellyseye"; public static final String THING_TYPE_SHELLYSENSE_STR = "shellysense"; public static final String THING_TYPE_SHELLYPROTECTED_STR = "shellydevice"; public static final String THING_TYPE_UNKNOWN_STR = "unknown"; // Device Types + public static final String SHELLYDT_SHELLY2 = "SHSW-21"; + public static final String SHELLYDT_SHELLY25 = "SHSW-25"; public static final String SHELLYDT_DIMMER = "SHDM-1"; + public static final String SHELLYDT_BULB = "SHBLB-1"; + public static final String SHELLYDT_DUO = "SHBDUO-1"; + public static final String SHELLYDT_VINTAGE = "SHVIN-1"; // List of all Thing Type UIDs public static final ThingTypeUID THING_TYPE_SHELLY1 = new ThingTypeUID(BINDING_ID, THING_TYPE_SHELLY1_STR); public static final ThingTypeUID THING_TYPE_SHELLY1PM = new ThingTypeUID(BINDING_ID, THING_TYPE_SHELLY1PN_STR); public static final ThingTypeUID THING_TYPE_SHELLYEM = new ThingTypeUID(BINDING_ID, THING_TYPE_SHELLYEM_STR); + public static final ThingTypeUID THING_TYPE_SHELLYEM3 = new ThingTypeUID(BINDING_ID, THING_TYPE_SHELLYEM3_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, @@ -80,10 +90,15 @@ public class ShellyBindingConstants { public static final ThingTypeUID THING_TYPE_SHELLYDIMMER = new ThingTypeUID(BINDING_ID, THING_TYPE_SHELLYDIMMER_STR); public static final ThingTypeUID THING_TYPE_SHELLYBULB = new ThingTypeUID(BINDING_ID, THING_TYPE_SHELLYBULB_STR); + public static final ThingTypeUID THING_TYPE_SHELLYDUO = new ThingTypeUID(BINDING_ID, THING_TYPE_SHELLYDUO_STR); + public static final ThingTypeUID THING_TYPE_SHELLYVINTAGE = new ThingTypeUID(BINDING_ID, + THING_TYPE_SHELLYVINTAGE_STR); public static final ThingTypeUID THING_TYPE_SHELLYHT = new ThingTypeUID(BINDING_ID, THING_TYPE_SHELLYHT_STR); public static final ThingTypeUID THING_TYPE_SHELLYSENSE = new ThingTypeUID(BINDING_ID, THING_TYPE_SHELLYSENSE_STR); public static final ThingTypeUID THING_TYPE_SHELLYSMOKE = new ThingTypeUID(BINDING_ID, THING_TYPE_SHELLYSMOKE_STR); public static final ThingTypeUID THING_TYPE_SHELLYFLOOD = new ThingTypeUID(BINDING_ID, THING_TYPE_SHELLYFLOOD_STR); + public static final ThingTypeUID THING_TYPE_SHELLYDOORWIN = new ThingTypeUID(BINDING_ID, + THING_TYPE_SHELLYDOORWIN_STR); public static final ThingTypeUID THING_TYPE_SHELLYEYE = new ThingTypeUID(BINDING_ID, THING_TYPE_SHELLYEYE_STR); public static final ThingTypeUID THING_TYPE_SHELLYRGBW2_COLOR = new ThingTypeUID(BINDING_ID, THING_TYPE_SHELLYRGBW2_COLOR_STR); @@ -93,21 +108,13 @@ public class ShellyBindingConstants { THING_TYPE_SHELLYPROTECTED_STR); public static final Set SUPPORTED_THING_TYPES_UIDS = Collections.unmodifiableSet( - Stream.of(THING_TYPE_SHELLY1, THING_TYPE_SHELLY1PM, THING_TYPE_SHELLYEM, THING_TYPE_SHELLY2_RELAY, - THING_TYPE_SHELLY2_ROLLER, THING_TYPE_SHELLY25_RELAY, THING_TYPE_SHELLY25_ROLLER, - THING_TYPE_SHELLY4PRO, THING_TYPE_SHELLYPLUG, THING_TYPE_SHELLYPLUGS, THING_TYPE_SHELLYDIMMER, - THING_TYPE_SHELLYBULB, THING_TYPE_SHELLYRGBW2_COLOR, THING_TYPE_SHELLYRGBW2_WHITE, - THING_TYPE_SHELLYHT, THING_TYPE_SHELLYSENSE, THING_TYPE_SHELLYEYE, THING_TYPE_SHELLYSMOKE, - THING_TYPE_SHELLYFLOOD, THING_TYPE_SHELLYUNKNOWN).collect(Collectors.toSet())); - - // check for updates every x sec - public static final int UPDATE_STATUS_INTERVAL_SECONDS = 3; - // update every x triggers or when a key was pressed - public static final int UPDATE_SKIP_COUNT = 20; - // update every x triggers or when a key was pressed - public static final int UPDATE_MIN_DELAY = 15; - // check for updates every x sec - public static final int UPDATE_SETTINGS_INTERVAL_SECONDS = 60; + Stream.of(THING_TYPE_SHELLY1, THING_TYPE_SHELLY1PM, THING_TYPE_SHELLYEM, THING_TYPE_SHELLYEM3, + THING_TYPE_SHELLY2_RELAY, THING_TYPE_SHELLY2_ROLLER, THING_TYPE_SHELLY25_RELAY, + THING_TYPE_SHELLY25_ROLLER, THING_TYPE_SHELLY4PRO, THING_TYPE_SHELLYPLUG, THING_TYPE_SHELLYPLUGS, + THING_TYPE_SHELLYDIMMER, THING_TYPE_SHELLYBULB, THING_TYPE_SHELLYDUO, THING_TYPE_SHELLYVINTAGE, + THING_TYPE_SHELLYRGBW2_COLOR, THING_TYPE_SHELLYRGBW2_WHITE, THING_TYPE_SHELLYHT, + THING_TYPE_SHELLYSENSE, THING_TYPE_SHELLYEYE, THING_TYPE_SHELLYSMOKE, THING_TYPE_SHELLYFLOOD, + THING_TYPE_SHELLYDOORWIN, THING_TYPE_SHELLYUNKNOWN).collect(Collectors.toSet())); // Thing Configuration Properties public static final String CONFIG_DEVICEIP = "deviceIp"; @@ -118,8 +125,9 @@ public class ShellyBindingConstants { public static final String PROPERTY_SERVICE_NAME = "serviceName"; public static final String PROPERTY_DEV_TYPE = "deviceType"; public static final String PROPERTY_DEV_MODE = "deviceMode"; - public static final String PROPERTY_HWBATCH = "hardwareBatch"; - public static final String PROPERTY_HWREV = "devHwRev"; + public static final String PROPERTY_HWREV = "deviceHwRev"; + public static final String PROPERTY_HWBATCH = "deviceHwBatch"; + public static final String PROPERTY_UPDATE_PERIOD = "devUpdatePeriod"; public static final String PROPERTY_NUM_RELAYS = "numberRelays"; public static final String PROPERTY_NUM_ROLLERS = "numberRollers"; public static final String PROPERTY_NUM_METER = "numberMeters"; @@ -133,6 +141,8 @@ public class ShellyBindingConstants { public static final String PROPERTY_COAP_DESCR = "coapDeviceDescr"; public static final String PROPERTY_STATS_TIMEOUTS = "statsTimeoutErrors"; public static final String PROPERTY_STATS_TRECOVERED = "statsTimeoutsRecovered"; + public static final String PROPERTY_COIOTAUTO = "coiotAutoEnable"; + public static final String PROPERTY_COIOTREFRESH = "coiotAutoRefresh"; // Relay public static final String CHANNEL_GROUP_RELAY_CONTROL = "relay"; @@ -146,12 +156,6 @@ public class ShellyBindingConstants { public static final String CHANNEL_TIMER_AUTOOFF = "autoOff"; public static final String CHANNEL_TIMER_ACTIVE = "timerActive"; - // External sensors for Shelly1/1PM - public static final String CHANNEL_GROUP_ETEMP_SENSORS = "sensors"; - public static final String CHANNEL_ETEMP_SENSOR1 = "temperature1"; - public static final String CHANNEL_ETEMP_SENSOR2 = "temperature2"; - public static final String CHANNEL_ETEMP_SENSOR3 = "temperature3"; - // Roller public static final String CHANNEL_GROUP_ROL_CONTROL = "roller"; public static final String CHANNEL_ROL_CONTROL_CONTROL = "control"; @@ -163,19 +167,19 @@ public class ShellyBindingConstants { // Dimmer public static final String CHANNEL_GROUP_DIMMER_CONTROL = CHANNEL_GROUP_RELAY_CONTROL; - public static final String CHANNEL_GROUP_DIMMER_STATUS = "status"; - public static final String CHANNEL_DIMMER_LOAD_ERROR = "loaderror"; - // Power meter public static final String CHANNEL_GROUP_METER = "meter"; public static final String CHANNEL_METER_CURRENTWATTS = "currentWatts"; - public static final String CHANNEL_METER_LASTMIN1 = "lastPower1"; - public static final String CHANNEL_METER_LASTMIN2 = "lastPower2"; - public static final String CHANNEL_METER_LASTMIN3 = "lastPower3"; + public static final String CHANNEL_METER_LASTMIN = "lastPower"; + public static final String CHANNEL_METER_LASTMIN1 = CHANNEL_METER_LASTMIN + "1"; + public static final String CHANNEL_METER_LASTMIN2 = CHANNEL_METER_LASTMIN + "2"; + public static final String CHANNEL_METER_LASTMIN3 = CHANNEL_METER_LASTMIN + "3"; public static final String CHANNEL_METER_TOTALKWH = "totalKWH"; public static final String CHANNEL_EMETER_TOTALRET = "returnedKWH"; public static final String CHANNEL_EMETER_REACTWATTS = "reactiveWatts"; public static final String CHANNEL_EMETER_VOLTAGE = "voltage"; + public static final String CHANNEL_EMETER_CURRENT = "current"; + public static final String CHANNEL_EMETER_PFACTOR = "powerFactor"; public static final String CHANNEL_GROUP_LED_CONTROL = "led"; public static final String CHANNEL_LED_STATUS_DISABLE = "statusLed"; @@ -185,9 +189,19 @@ public class ShellyBindingConstants { public static final String CHANNEL_SENSOR_TEMP = "temperature"; public static final String CHANNEL_SENSOR_HUM = "humidity"; public static final String CHANNEL_SENSOR_LUX = "lux"; + public static final String CHANNEL_SENSOR_ILLUM = "illumination"; + public static final String CHANNEL_SENSOR_VIBRATION = "vibration"; + public static final String CHANNEL_SENSOR_TILT = "tilt"; public static final String CHANNEL_SENSOR_FLOOD = "flood"; + public static final String CHANNEL_SENSOR_SMOKE = "smoke"; + public static final String CHANNEL_SENSOR_STATE = "state"; public static final String CHANNEL_SENSOR_MOTION = "motion"; - public static final String CHANNEL_SENSOR_CHARGER = "charger"; + public static final String CHANNEL_SENSOR_ERROR = "lastError"; + // External sensors for Shelly1/1PM + public static final String CHANNEL_ESENDOR_TEMP1 = CHANNEL_SENSOR_TEMP + "1"; + public static final String CHANNEL_ESENDOR_TEMP2 = CHANNEL_SENSOR_TEMP + "2"; + public static final String CHANNEL_ESENDOR_TEMP3 = CHANNEL_SENSOR_TEMP + "3"; + public static final String CHANNEL_ESENDOR_HUMIDITY = CHANNEL_SENSOR_HUM; public static final String CHANNEL_GROUP_SENSE_CONTROL = "control"; public static final String CHANNEL_SENSE_KEY = "key"; @@ -214,7 +228,7 @@ public class ShellyBindingConstants { public static final String CHANNEL_COLOR_GAIN = "gain"; public static final String CHANNEL_COLOR_EFFECT = "effect"; - // Bulb/RGBW2 in White Mode + // Bulb/RGBW2/Dup in White Mode public static final String CHANNEL_GROUP_WHITE_CONTROL = "white"; public static final String CHANNEL_COLOR_TEMP = "temperature"; @@ -222,7 +236,13 @@ public class ShellyBindingConstants { public static final String CHANNEL_GROUP_DEV_STATUS = "device"; public static final String CHANNEL_DEVST_UPTIME = "uptime"; public static final String CHANNEL_DEVST_RSSI = "wifiSignal"; + public static final String CHANNEL_DEVST_ITEMP = "internalTemp"; + public static final String CHANNEL_DEVST_WAKEUP = "wakeupReason"; public static final String CHANNEL_DEVST_ALARM = "alarm"; + public static final String CHANNEL_DEVST_ACCUWATTS = "accumulatedWatts"; + public static final String CHANNEL_DEVST_ACCUTOTAL = "accumulatedWTotal"; + public static final String CHANNEL_DEVST_ACCURETURNED = "accumulatedReturned"; + public static final String CHANNEL_DEVST_CHARGER = "charger"; // General public static final String CHANNEL_LAST_UPDATE = "lastUpdate"; @@ -230,8 +250,8 @@ public class ShellyBindingConstants { public static final String CHANNEL_BUTTON_TRIGGER = "button"; public static final String SERVICE_TYPE = "_http._tcp.local."; - public static final String SHELLY_API_MIN_FWVERSION = "v1.5.2"; - public static final int SHELLY_API_TIMEOUT_MS = 5000; + public static final String SHELLY_API_MIN_FWVERSION = "v1.5.7";// v1.5.7+ + public static final String SHELLY_API_MIN_FWCOIOT = "v1.6";// v1.6.0+ // Alarm types/messages public static final String ALARM_TYPE_NONE = "NONE"; @@ -242,78 +262,30 @@ public class ShellyBindingConstants { public static final String ALARM_TYPE_LOADERR = "LOAD_ERROR"; public static final String ALARM_TYPE_LOW_BATTERY = "LOW_BATTERY"; - // Coap - public static final int COIOT_PORT = 5683; - public static final String COAP_MULTICAST_ADDRESS = "224.0.1.187"; - - public static final String COLOIT_URI_BASE = "/cit/"; - public static final String COLOIT_URI_DEVDESC = COLOIT_URI_BASE + "d"; - public static final String COLOIT_URI_DEVSTATUS = COLOIT_URI_BASE + "s"; - - public static final int COIOT_OPTION_GLOBAL_DEVID = 3332; - public static final int COIOT_OPTION_STATUS_VALIDITY = 3412; - public static final int COIOT_OPTION_STATUS_SERIAL = 3420; - - public static final byte[] EMPTY_BYTE = new byte[0]; - - public static final String SHELLY_NULL_URL = "null"; - public static final String SHELLY_URL_DEVINFO = "/shelly"; - public static final String SHELLY_URL_STATUS = "/status"; - public static final String SHELLY_URL_SETTINGS = "/settings"; - public static final String SHELLY_URL_SETTINGS_AP = "/settings/ap"; - public static final String SHELLY_URL_SETTINGS_STA = "/settings/sta"; - public static final String SHELLY_URL_SETTINGS_LOGIN = "/settings/sta"; - public static final String SHELLY_URL_SETTINGS_CLOUD = "/settings/cloud"; - public static final String SHELLY_URL_LIST_IR = "/ir/list"; - public static final String SHELLY_URL_SEND_IR = "/ir/emit"; - - public static final String SHELLY_URL_SETTINGS_RELAY = "/settings/relay"; - public static final String SHELLY_URL_STATUS_RELEAY = "/status/relay"; - public static final String SHELLY_URL_CONTROL_RELEAY = "/relay"; - - public static final String SHELLY_URL_SETTINGS_EMETER = "/settings/emeter"; - public static final String SHELLY_URL_STATUS_EMETER = "/emeter"; - public static final String SHELLY_URL_DATA_EMETER = "/emeter/{0}/em_data.csv"; - - public static final String SHELLY_URL_CONTROL_ROLLER = "/roller"; - public static final String SHELLY_URL_SETTINGS_ROLLER = "/settings/roller"; - - public static final String SHELLY_URL_SETTINGS_LIGHT = "/settings/light"; - public static final String SHELLY_URL_STATUS_LIGHT = "/light"; - public static final String SHELLY_URL_CONTROL_LIGHT = "/light"; - - public static final String SHELLY_URL_SETTINGS_DIMMER = "/settings/light"; - - public static final String SHELLY_CALLBACK_URI = "/shelly/event"; + // Event types public static final String EVENT_TYPE_RELAY = "relay"; public static final String EVENT_TYPE_ROLLER = "roller"; - public static final String EVENT_TYPE_SENSORDATA = "sensordata"; public static final String EVENT_TYPE_LIGHT = "light"; + public static final String EVENT_TYPE_SENSORDATA = "report"; - public static final String SHELLY_IR_CODET_STORED = "stored"; - public static final String SHELLY_IR_CODET_PRONTO = "pronto"; - public static final String SHELLY_IR_CODET_PRONTO_HEX = "pronto_hex"; - - public static final String HTTP_DELETE = "DELETE"; - public static final String HTTP_HEADER_AUTH = "Authorization"; - public static final String HTTP_AUTH_TYPE_BASIC = "Basic"; - public static final String CONTENT_TYPE_XML = "text/xml; charset=UTF-8"; - - public static final String APIERR_HTTP_401_UNAUTHORIZED = "401 Unauthorized"; - public static final String APIERR_TIMEOUT = "Timeout"; - public static final String APIERR_NOT_CALIBRATED = "Not calibrated!"; - - // Minimum signal strength for basic connectivity. Packet delivery may be unreliable. - public static final int HEALTH_CHECK_INTERVAL_SEC = 300; + // URI for the EventServlet + public static final String SHELLY_CALLBACK_URI = "/shelly/event"; public static final int DIM_STEPSIZE = 5; // Formatting: Number of scaling digits public static final int DIGITS_NONE = 0; - public static final int DIGITS_WATT = 3; - public static final int DIGITS_KWH = 4; - public static final int DIGITS_VOLT = 2; - public static final int DIGITS_TEMP = 2; - public static final int DIGITS_LUX = 2; - public static final int DIGITS_PERCENT = 2; + public static final int DIGITS_WATT = 1; + public static final int DIGITS_KWH = 3; + public static final int DIGITS_VOLT = 1; + public static final int DIGITS_TEMP = 1; + public static final int DIGITS_LUX = 1; + public static final int DIGITS_PERCENT = 1; + + public static final int SHELLY_API_TIMEOUT_MS = 5000; + public static final int UPDATE_STATUS_INTERVAL_SECONDS = 3; // check for updates every x sec + public static final int UPDATE_SKIP_COUNT = 20; // update every x triggers or when a key was pressed + public static final int UPDATE_MIN_DELAY = 15;// update every x triggers or when a key was pressed + public static final int UPDATE_SETTINGS_INTERVAL_SECONDS = 60; // check for updates every x sec + public static final int HEALTH_CHECK_INTERVAL_SEC = 300; // Health check interval, 5min } diff --git a/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/ShellyHandlerFactory.java b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/ShellyHandlerFactory.java index fbd599949cee3..3ba6d4bd1f8be 100755 --- a/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/ShellyHandlerFactory.java +++ b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/ShellyHandlerFactory.java @@ -16,11 +16,13 @@ import java.util.Map; import java.util.Set; -import java.util.concurrent.CopyOnWriteArraySet; -import org.apache.commons.lang.Validate; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.util.ConcurrentHashSet; +import org.eclipse.smarthome.core.i18n.LocaleProvider; +import org.eclipse.smarthome.core.i18n.TranslationProvider; import org.eclipse.smarthome.core.net.HttpServiceUtil; import org.eclipse.smarthome.core.net.NetworkAddressService; import org.eclipse.smarthome.core.thing.Thing; @@ -28,13 +30,15 @@ import org.eclipse.smarthome.core.thing.binding.BaseThingHandlerFactory; import org.eclipse.smarthome.core.thing.binding.ThingHandler; import org.eclipse.smarthome.core.thing.binding.ThingHandlerFactory; +import org.eclipse.smarthome.io.net.http.HttpClientFactory; import org.openhab.binding.shelly.internal.coap.ShellyCoapServer; import org.openhab.binding.shelly.internal.config.ShellyBindingConfiguration; import org.openhab.binding.shelly.internal.handler.ShellyBaseHandler; -import org.openhab.binding.shelly.internal.handler.ShellyDeviceListener; import org.openhab.binding.shelly.internal.handler.ShellyLightHandler; import org.openhab.binding.shelly.internal.handler.ShellyProtectedHandler; import org.openhab.binding.shelly.internal.handler.ShellyRelayHandler; +import org.openhab.binding.shelly.internal.util.ShellyTranslationProvider; +import org.openhab.binding.shelly.internal.util.ShellyUtils; import org.osgi.service.component.ComponentContext; import org.osgi.service.component.annotations.Activate; import org.osgi.service.component.annotations.Component; @@ -53,9 +57,10 @@ @Component(service = { ThingHandlerFactory.class, ShellyHandlerFactory.class }, configurationPid = "binding.shelly") public class ShellyHandlerFactory extends BaseThingHandlerFactory { private final Logger logger = LoggerFactory.getLogger(ShellyHandlerFactory.class); + private final HttpClient httpClient; + private final ShellyTranslationProvider messages; private final ShellyCoapServer coapServer; - private final Set deviceListeners = new CopyOnWriteArraySet<>(); - + private final Set deviceListeners = new ConcurrentHashSet<>(); private static final Set SUPPORTED_THING_TYPES_UIDS = ShellyBindingConstants.SUPPORTED_THING_TYPES_UIDS; private ShellyBindingConfiguration bindingConfig = new ShellyBindingConfiguration(); private String localIP = ""; @@ -70,24 +75,26 @@ public class ShellyHandlerFactory extends BaseThingHandlerFactory { */ @Activate public ShellyHandlerFactory(@Reference NetworkAddressService networkAddressService, - ComponentContext componentContext, Map configProperties) { + @Reference LocaleProvider localeProvider, @Reference TranslationProvider i18nProvider, + @Reference HttpClientFactory httpClientFactory, ComponentContext componentContext, + Map configProperties) { logger.debug("Activate Shelly HandlerFactory"); super.activate(componentContext); - this.coapServer = new ShellyCoapServer(); - Validate.notNull(coapServer, "coapServer creation failed!"); + messages = new ShellyTranslationProvider(bundleContext.getBundle(), i18nProvider, localeProvider); + localIP = ShellyUtils.getString(networkAddressService.getPrimaryIpv4HostAddress().toString()); - Validate.notNull(configProperties); - bindingConfig.updateFromProperties(configProperties); + this.httpClient = httpClientFactory.getCommonHttpClient(); httpPort = HttpServiceUtil.getHttpServicePort(componentContext.getBundleContext()); if (httpPort == -1) { httpPort = 8080; } - Validate.isTrue(httpPort > 0, "Unable to get OH HTTP port"); logger.debug("Using OH HTTP port {}", httpPort); - String lip = networkAddressService.getPrimaryIpv4HostAddress(); - localIP = lip != null ? lip : ""; + this.coapServer = new ShellyCoapServer(); + + // Save bindingConfig & pass it to all registered listeners + bindingConfig.updateFromProperties(configProperties); } @Override @@ -102,16 +109,20 @@ public boolean supportsThingType(ThingTypeUID thingTypeUID) { ShellyBaseHandler handler = null; if (thingType.equals(THING_TYPE_SHELLYPROTECTED_STR)) { - logger.debug("Create new thing of type {} using ShellyRelayHandler", thingTypeUID.getId()); - handler = new ShellyProtectedHandler(thing, bindingConfig, coapServer, localIP, httpPort); - } else if (thingType.equals(THING_TYPE_SHELLYBULB.getId()) + logger.debug("{}: Create new thing of type {} using ShellyProtectedHandler", thing.getLabel(), + thingTypeUID.toString()); + handler = new ShellyProtectedHandler(thing, messages, bindingConfig, coapServer, localIP, httpPort, + httpClient); + } else if (thingType.equals(THING_TYPE_SHELLYBULB.getId()) || thingType.equals(THING_TYPE_SHELLYDUO.getId()) || thingType.equals(THING_TYPE_SHELLYRGBW2_COLOR.getId()) || thingType.equals(THING_TYPE_SHELLYRGBW2_WHITE.getId())) { - logger.debug("Create new thing of type {} using ShellyLightHandler", thingTypeUID.getId()); - handler = new ShellyLightHandler(thing, bindingConfig, coapServer, localIP, httpPort); + logger.debug("{}: Create new thing of type {} using ShellyLightHandler", thing.getLabel(), + thingTypeUID.toString()); + handler = new ShellyLightHandler(thing, messages, bindingConfig, coapServer, localIP, httpPort, httpClient); } else if (SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID)) { - logger.debug("Create new thing of type {} using ShellyRelayHandler", thingTypeUID.getId()); - handler = new ShellyRelayHandler(thing, bindingConfig, coapServer, localIP, httpPort); + logger.debug("{}: Create new thing of type {} using ShellyRelayHandler", thing.getLabel(), + thingTypeUID.toString()); + handler = new ShellyRelayHandler(thing, messages, bindingConfig, coapServer, localIP, httpPort, httpClient); } if (handler != null) { @@ -126,7 +137,6 @@ public boolean supportsThingType(ThingTypeUID thingTypeUID) { /** * Remove handler of things. */ - @SuppressWarnings("unlikely-arg-type") @Override protected synchronized void removeHandler(@NonNull ThingHandler thingHandler) { if (thingHandler instanceof ShellyBaseHandler) { @@ -142,24 +152,17 @@ protected synchronized void removeHandler(@NonNull ThingHandler thingHandler) { * @param eventType Type of event, e.g. light * @param parameters Input parameters from URL, e.g. on sensor reports */ - public void onEvent(String deviceName, String componentIndex, String eventType, Map parameters) { - logger.trace("Dispatch event to device handler {}", deviceName); - for (ShellyDeviceListener listener : deviceListeners) { - try { - if (listener.onEvent(deviceName, componentIndex, eventType, parameters)) { - // event processed - break; - } - } catch (NullPointerException e) { - logger.debug("Unable to process callback: {} ({}), deviceName={}, type={}, index={}, parameters={}\n{}", - e.getMessage(), e.getClass(), deviceName, eventType, componentIndex, parameters.toString(), - e.getStackTrace()); - // continue with next listener + public void onEvent(String ipAddress, String deviceName, String componentIndex, String eventType, + Map parameters) { + logger.trace("{}: Dispatch event to thing handler", deviceName); + for (ShellyBaseHandler listener : deviceListeners) { + if (listener.onEvent(ipAddress, deviceName, componentIndex, eventType, parameters)) { + // event processed + return; } } } - @Nullable public ShellyBindingConfiguration getBindingConfig() { return bindingConfig; } 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 new file mode 100644 index 0000000000000..b52557e842be5 --- /dev/null +++ b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api/ShellyApiException.java @@ -0,0 +1,133 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.shelly.internal.api; + +import java.net.MalformedURLException; +import java.net.UnknownHostException; +import java.text.MessageFormat; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeoutException; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +import com.google.gson.JsonSyntaxException; + +/** + * The {@link CarNetException} implements an extension to the standard Exception class. This allows to keep also the + * result of the last API call (e.g. including the http status code in the message). + * + * @author Markus Michels - Initial contribution + */ +@NonNullByDefault +public class ShellyApiException extends Exception { + private static final long serialVersionUID = -5809459454769761821L; + + private ShellyApiResult apiResult = new ShellyApiResult(); + private static String EX_NONE = "none"; + + public ShellyApiException(Exception exception) { + super(exception); + } + + public ShellyApiException(String message) { + super(message); + } + + public ShellyApiException(ShellyApiResult res) { + super(EX_NONE); + apiResult = res; + } + + public ShellyApiException(Exception exception, String message) { + super(message, exception); + } + + public ShellyApiException(ShellyApiResult result, Exception exception) { + super(exception); + apiResult = result; + } + + @Override + public String getMessage() { + return isEmpty() ? "" : nonNullString(super.getMessage()); + } + + @Override + public String toString() { + String message = nonNullString(super.getMessage()); + String cause = getCauseClass().toString(); + if (!isEmpty()) { + if (isUnknownHost()) { + String[] string = message.split(": "); // java.net.UnknownHostException: api.rach.io + message = MessageFormat.format("Unable to connect to {0} (Unknown host / Network down / Low signal)", + string[1]); + } else if (isMalformedURL()) { + message = MessageFormat.format("Invalid URL: {0}", apiResult.getUrl()); + } else if (isTimeout()) { + message = MessageFormat.format("Device unreachable or API Timeout ({0})", apiResult.getUrl()); + } else { + message = MessageFormat.format("{0} ({1})", message, cause); + } + } else { + message = apiResult.toString(); + } + return message; + } + + public boolean isApiException() { + return getCauseClass() == ShellyApiException.class; + } + + public boolean isTimeout() { + Class extype = !isEmpty() ? getCauseClass() : null; + return (extype != null) && ((extype == TimeoutException.class) || (extype == ExecutionException.class) + || (extype == InterruptedException.class) || getMessage().toLowerCase().contains("timeout")); + } + + public boolean isHttpAccessUnauthorized() { + return apiResult.isHttpAccessUnauthorized(); + } + + public boolean isUnknownHost() { + return getCauseClass() == MalformedURLException.class; + } + + public boolean isMalformedURL() { + return getCauseClass() == UnknownHostException.class; + } + + public boolean IsJSONException() { + return getCauseClass() == JsonSyntaxException.class; + } + + public ShellyApiResult getApiResult() { + return apiResult; + } + + private boolean isEmpty() { + return nonNullString(super.getMessage()).equals(EX_NONE); + } + + private static String nonNullString(@Nullable String s) { + return s != null ? s : ""; + } + + private Class getCauseClass() { + Throwable cause = getCause(); + if (getCause() != null) { + return cause.getClass(); + } + return ShellyApiException.class; + } +} diff --git a/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api/ShellyApiJsonDTO.java b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api/ShellyApiJsonDTO.java index 27349e9e880df..8a591ac14d8c6 100644 --- a/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api/ShellyApiJsonDTO.java +++ b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api/ShellyApiJsonDTO.java @@ -22,15 +22,174 @@ * @author Markus Michels - Initial contribution */ public class ShellyApiJsonDTO { + public static final String SHELLY_NULL_URL = "null"; + public static final String SHELLY_URL_DEVINFO = "/shelly"; + public static final String SHELLY_URL_STATUS = "/status"; + public static final String SHELLY_URL_SETTINGS = "/settings"; + public static final String SHELLY_URL_SETTINGS_AP = "/settings/ap"; + public static final String SHELLY_URL_SETTINGS_STA = "/settings/sta"; + public static final String SHELLY_URL_SETTINGS_LOGIN = "/settings/sta"; + public static final String SHELLY_URL_SETTINGS_CLOUD = "/settings/cloud"; + public static final String SHELLY_URL_LIST_IR = "/ir/list"; + public static final String SHELLY_URL_SEND_IR = "/ir/emit"; + + public static final String SHELLY_URL_SETTINGS_RELAY = "/settings/relay"; + public static final String SHELLY_URL_STATUS_RELEAY = "/status/relay"; + public static final String SHELLY_URL_CONTROL_RELEAY = "/relay"; + + public static final String SHELLY_URL_SETTINGS_EMETER = "/settings/emeter"; + public static final String SHELLY_URL_STATUS_EMETER = "/emeter"; + public static final String SHELLY_URL_DATA_EMETER = "/emeter/{0}/em_data.csv"; + + public static final String SHELLY_URL_CONTROL_ROLLER = "/roller"; + public static final String SHELLY_URL_SETTINGS_ROLLER = "/settings/roller"; + + public static final String SHELLY_URL_SETTINGS_LIGHT = "/settings/light"; + public static final String SHELLY_URL_STATUS_LIGHT = "/light"; + public static final String SHELLY_URL_CONTROL_LIGHT = "/light"; + + public static final String SHELLY_URL_SETTINGS_DIMMER = "/settings/light"; + + // + // Action URLs according to the device type + // + public static final String SHELLY_EVENTURL_SUFFIX = "_url"; + + // Relay + public static final String SHELLY_EVENT_BTN_ON = "btn_on"; + public static final String SHELLY_EVENT_BTN_OFF = "btn_off"; + public static final String SHELLY_EVENT_OUT_ON = "out_on"; + public static final String SHELLY_EVENT_OUT_OFF = "out_off"; + public static final String SHELLY_EVENT_SHORTPUSH = "shortpush"; + public static final String SHELLY_EVENT_LONGPUSH = "longpush"; + + // Dimmer + public static final String SHELLY_EVENT_BTN1_ON = "btn1_on"; + public static final String SHELLY_EVENT_BTN1_OFF = "btn1_off"; + public static final String SHELLY_EVENT_BTN2_ON = "btn2_on"; + public static final String SHELLY_EVENT_BTN2_OFF = "btn2_off"; + public static final String SHELLY_EVENT_SHORTPUSH1 = "btn1_shortpush"; + public static final String SHELLY_EVENT_LONGPUSH1 = "btn1_longpush"; + public static final String SHELLY_EVENT_SHORTPUSH2 = "btn2_shortpush"; + public static final String SHELLY_EVENT_LONGPUSH2 = "btn2_longpush"; + + // Roller + public static final String SHELLY_EVENT_ROLLER_OPEN = "roller_open"; + public static final String SHELLY_EVENT_ROLLER_CLOSE = "roller_close"; + public static final String SHELLY_EVENT_ROLLER_STOP = "roller_stop"; + + // Sensors + public static final String SHELLY_EVENT_SENSORREPORT = "report"; + public static final String SHELLY_EVENT_DARK = "dark"; + public static final String SHELLY_EVENT_TWILIGHT = "twilight"; + public static final String SHELLY_EVENT_FLOOD_DETECTED = "flood_detected"; + public static final String SHELLY_EVENT_FLOOD_GONE = "flood_gone"; + public static final String SHELLY_EVENT_VIBRATION = "vibration"; // DW 1.6.5+ + public static final String SHELLY_EVENT_CLOSE = "close_url"; // DW 1.6.5+ + + // + // API values + // + public static final String SHELLY_BTNT_MOMENTARY = "momentary"; + public static final String SHELLY_BTNT_TOGGLE = "toggle"; + public static final String SHELLY_BTNT_EDGE = "edge"; + public static final String SHELLY_BTNT_DETACHED = "detached"; + public static final String SHELLY_STATE_LAST = "last"; + public static final String SHELLY_STATE_STOP = "stop"; + public static final String SHELLY_INP_MODE_OPENCLOSE = "openclose"; + public static final String SHELLY_OBSTMODE_DISABLED = "disabled"; + public static final String SHELLY_SAFETYM_WHILEOPENING = "while_opening"; + public static final String SHELLY_ALWD_TRIGGER_NONE = "none"; + public static final String SHELLY_ALWD_ROLLER_TURN_OPEN = "open"; + public static final String SHELLY_ALWD_ROLLER_TURN_CLOSE = "close"; + public static final String SHELLY_ALWD_ROLLER_TURN_STOP = "stop"; + + // API Error Codes + public static final String SHELLY_APIERR_UNAUTHORIZED = "Unauthorized"; + public static final String SHELLY_APIERR_TIMEOUT = "Timeout"; + public static final String SHELLY_APIERR_NOT_CALIBRATED = "Not calibrated!"; + + // API device types / properties + public static final String SHELLY_CLASS_RELAY = "relay"; // Relay: relay mode + public static final String SHELLY_CLASS_ROLLER = "roller"; // Relay: roller mode + public static final String SHELLY_CLASS_LIGHT = "light"; // Bulb: color mode public static final String SHELLY_API_ON = "on"; public static final String SHELLY_API_OFF = "off"; public static final String SHELLY_API_TRUE = "true"; public static final String SHELLY_API_FALSE = "false"; - public static final String SHELLY_CLASS_RELAY = "relay"; // Relay: relay mode - public static final String SHELLY_CLASS_ROLLER = "roller"; // Relay: roller mode - public static final String SHELLY_CLASS_LIGHT = "light"; // Bulb: color mode + public static final String SHELLY_API_MODE = "mode"; + public static final String SHELLY_MODE_RELAY = "relay"; // Relay: relay mode + public static final String SHELLY_MODE_ROLLER = "roller"; // Relay: roller mode + public static final String SHELLY_MODE_COLOR = "color"; // Bulb/RGBW2: color mode + public static final String SHELLY_MODE_WHITE = "white"; // Bulb/RGBW2: white mode + + public static final String SHELLY_LED_STATUS_DISABLE = "led_status_disable"; + public static final String SHELLY_LED_POWER_DISABLE = "led_power_disable"; + + public static final String SHELLY_API_STOPR_NORMAL = "normal"; + public static final String SHELLY_API_STOPR_SAFETYSW = "safety_switch"; + public static final String SHELLY_API_STOPR_OBSTACLE = "obstacle"; + + public static final String SHELLY_TIMER_AUTOON = "auto_on"; + public static final String SHELLY_TIMER_AUTOOFF = "auto_off"; + public static final String SHELLY_TIMER_ACTIVE = "has_timer"; + + public static final String SHELLY_LIGHT_TURN = "turn"; + public static final String SHELLY_LIGHT_DEFSTATE = "def_state"; + public static final String SHELLY_LIGHTTIMER = "timer"; + + public static final String SHELLY_COLOR_RED = "red"; + public static final String SHELLY_COLOR_BLUE = "blue"; + public static final String SHELLY_COLOR_GREEN = "green"; + public static final String SHELLY_COLOR_YELLOW = "yellow"; + public static final String SHELLY_COLOR_WHITE = "white"; + public static final String SHELLY_COLOR_GAIN = "gain"; + public static final String SHELLY_COLOR_BRIGHTNESS = "brightness"; + public static final String SHELLY_COLOR_TEMP = "temp"; + public static final String SHELLY_COLOR_EFFECT = "effect"; + + public static final int SHELLY_MIN_ROLLER_POS = 0; + public static final int SHELLY_MAX_ROLLER_POS = 100; + public static final int SHELLY_MIN_BRIGHTNESS = 0; + public static final int SHELLY_MAX_BRIGHTNESS = 100; + public static final int SHELLY_MIN_GAIN = 0; + public static final int SHELLY_MAX_GAIN = 100; + public static final int SHELLY_MIN_COLOR = 0; + public static final int SHELLY_MAX_COLOR = 255; + public static final int SHELLY_DIM_STEPSIZE = 10; + + // color temperature: 3000 = warm, 4750 = white, 6565 = cold; gain: 0..100 + public static final int MIN_COLOR_TEMP_BULB = 3000; + public static final int MAX_COLOR_TEMP_BULB = 6500; + public static final int MIN_COLOR_TEMP_DUO = 2700; + public static final int MAX_COLOR_TEMP_DUO = 6500; + public static final int COLOR_TEMP_RANGE_BULB = MAX_COLOR_TEMP_DUO - MIN_COLOR_TEMP_DUO; + public static final int COLOR_TEMP_RANGE_DUO = MAX_COLOR_TEMP_DUO - MIN_COLOR_TEMP_DUO; + public static final double MIN_BRIGHTNESS = 0.0; + public static final double MAX_BRIGHTNESS = 100.0; + public static final double SATURATION_FACTOR = 2.55; + public static final double GAIN_FACTOR = SHELLY_MAX_GAIN / 100; + public static final double BRIGHTNESS_FACTOR = SHELLY_MAX_BRIGHTNESS / 100; + + // Door/Window + public static final String SHELLY_API_ILLUM_DARK = "dark"; + public static final String SHELLY_API_ILLUM_TWILIGHT = "twilight"; + public static final String SHELLY_API_ILLUM_BRIGHT = "bright"; + public static final String SHELLY_API_DWSTATE_OPEN = "open"; + public static final String SHELLY_API_DWSTATE_CLOSE = "close"; + + // Shelly Sense + public static final String SHELLY_IR_CODET_STORED = "stored"; + public static final String SHELLY_IR_CODET_PRONTO = "pronto"; + public static final String SHELLY_IR_CODET_PRONTO_HEX = "pronto_hex"; + + public static final int SHELLY_MIN_EFFECT = 0; + public static final int SHELLY_MAX_EFFECT = 6; + + public static final String SHELLY_TEMP_CELSIUS = "C"; + public static final String SHELLY_TEMP_FAHRENHEIT = "F"; public static class ShellySettingsDevice { public String type; @@ -61,9 +220,6 @@ public static class ShellySettingsWiFiNetwork { @SerializedName("ipv4_method") public String ipv4Method; - public final String SHELLY_IPM_STATIC = "static"; - public final String SHELLY_IPM_DHCP = "dhcp"; - public String ip; public String gw; public String mask; @@ -93,6 +249,11 @@ public static class ShellySettingsMqtt { public Integer updatePeriod; } + public static class ShellySettingsCoiot { // FW 1.6+ + @SerializedName("update_period") + public Integer updatePeriod; + } + public static class ShellySettingsSntp { public String server; } @@ -200,50 +361,6 @@ public static class ShellySettingsDimmer { public Integer swapInputs; // 0=no } - public static final String SHELLY_API_EVENTURL_BTN_ON = "btn_on_url"; - public static final String SHELLY_API_EVENTURL_BTN_OFF = "btn_off_url"; - public static final String SHELLY_API_EVENTURL_BTN1_ON = "btn1_on_url"; - public static final String SHELLY_API_EVENTURL_BTN1_OFF = "btn1_off_url"; - public static final String SHELLY_API_EVENTURL_BTN2_ON = "btn2_on_url"; - public static final String SHELLY_API_EVENTURL_BTN2_OFF = "btn2_off_url"; - public static final String SHELLY_API_EVENTURL_OUT_ON = "out_on_url"; - public static final String SHELLY_API_EVENTURL_OUT_OFF = "out_off_url"; - public static final String SHELLY_API_EVENTURL_SHORT_PUSH = "shortpush_url"; - public static final String SHELLY_API_EVENTURL_LONG_PUSH = "longpush_url"; - public static final String SHELLY_API_EVENTURL_ROLLER_OPEN = "roller_open_url"; - public static final String SHELLY_API_EVENTURL_ROLLER_CLOSE = "roller_close_url"; - public static final String SHELLY_API_EVENTURL_ROLLER_STOP = "roller_stop_url"; - public static final String SHELLY_API_EVENTURL_REPORT = "report_url"; - - public static final String SHELLY_EVENT_BTN_ON = "btn_on"; - public static final String SHELLY_EVENT_BTN_OFF = "btn_off"; - public static final String SHELLY_EVENT_BTN1_OFF = "btn1_on"; - public static final String SHELLY_EVENT_BTN1_ON = "btn1_off"; - public static final String SHELLY_EVENT_BTN2_ON = "btn2_on"; - public static final String SHELLY_EVENT_BTN2_OFF = "btn2_off"; - public static final String SHELLY_EVENT_SHORTPUSH = "shortpush"; - public static final String SHELLY_EVENT_LONGPUSH = "longpush"; - public static final String SHELLY_EVENT_OUT_ON = "out_on"; - public static final String SHELLY_EVENT_OUT_OFF = "out_off"; - public static final String SHELLY_EVENT_ROLLER_OPEN = "roller_open"; - public static final String SHELLY_EVENT_ROLLER_CLOSE = "roller_close"; - public static final String SHELLY_EVENT_ROLLER_STOP = "roller_stop"; - public static final String SHELLY_EVENT_SENSORDATA = "sensordata"; - - public static final String SHELLY_BTNT_MOMENTARY = "momentary"; - public static final String SHELLY_BTNT_TOGGLE = "toggle"; - public static final String SHELLY_BTNT_EDGE = "edge"; - public static final String SHELLY_BTNT_DETACHED = "detached"; - public static final String SHELLY_STATE_LAST = "last"; - public static final String SHELLY_STATE_STOP = "stop"; - public static final String SHELLY_INP_MODE_OPENCLOSE = "openclose"; - public static final String SHELLY_OBSTMODE_DISABLED = "disabled"; - public static final String SHELLY_SAFETYM_WHILEOPENING = "while_opening"; - public static final String SHELLY_ALWD_TRIGGER_NONE = "none"; - public static final String SHELLY_ALWD_ROLLER_TURN_OPEN = "open"; - public static final String SHELLY_ALWD_ROLLER_TURN_CLOSE = "close"; - public static final String SHELLY_ALWD_ROLLER_TURN_STOP = "stop"; - public static class ShellySettingsRoller { public Double maxtime; @SerializedName("maxtime_open") @@ -310,6 +427,9 @@ public static class ShellySettingsEMeter { // ShellyEM meter public Double total; // Total consumed energy, Wh @SerializedName("total_returned") public Double totalReturned; // Total returned energy, Wh + + public Double pf; // EM3 + public Double current; // EM3 } public static class ShellySettingsUpdate { @@ -333,21 +453,34 @@ public static class ShellySettingsGlobal { public ShellySettingsWiFiNetwork wifiSta1; // public ShellySettingsMqtt mqtt; // not used for now // public ShellySettingsSntp sntp; // not used for now + public ShellySettingsCoiot coiot; // Firmware 1.6+ public ShellySettingsLogin login; @SerializedName("pin_code") public String pinCode; @SerializedName("coiot_execute_enable") public Boolean coiotExecuteEnable; public String name; + public Boolean discoverable; // FW 1.6+ public String fw; @SerializedName("build_info") ShellySettingsBuildInfo buildInfo; ShellyStatusCloud cloud; + @SerializedName("sleep_mode") + public ShellySensorSleepMode sleepMode; // FW 1.6 + public String timezone; public Double lat; public Double lng; public Boolean tzautodetect; public String time; + // @SerializedName("tz_utc_offset") + // public Integer tzUTCOoffset; // FW 1.6+ + // @SerializedName("tz_dst") + // public Boolean tzDdst; // FW 1.6+ + // @SerializedName("tz_dst_auto") + // public Boolean tzDstAuto; // FW 1.6+ + // public Long unixtime; // FW 1.6+ + public ShellySettingsHwInfo hwinfo; public String mode; @SerializedName("max_power") @@ -358,6 +491,9 @@ public static class ShellySettingsGlobal { public ArrayList emeters; public ArrayList inputs; // Firmware 1.5.6+ + @SerializedName("temperature_units") + public String temperatureUnits; // Either'C'or'F' + @SerializedName("led_status_disable") public Boolean ledStatusDisable; // PlugS only Disable LED indication for network // status @@ -368,16 +504,30 @@ public static class ShellySettingsGlobal { public String lightSensor; // Sense: sensor type @SerializedName("rain_sensor") public Boolean rainSensor; // Flood: true=in rain mode - } - public static final String SHELLY_API_MODE = "mode"; - public static final String SHELLY_MODE_RELAY = "relay"; // Relay: relay mode - public static final String SHELLY_MODE_ROLLER = "roller"; // Relay: roller mode - public static final String SHELLY_MODE_COLOR = "color"; // Bulb/RGBW2: color mode - public static final String SHELLY_MODE_WHITE = "white"; // Bulb/RGBW2: white mode - - public static final String SHELLY_LED_STATUS_DISABLE = "led_status_disable"; - public static final String SHELLY_LED_POWER_DISABLE = "led_power_disable"; + // FW 1.5.7: Door Window + @SerializedName("dark_treshold") + public Integer darkTreshold; // Illumination definition for "dark" in lux + @SerializedName("twilight_treshold") + public Integer twiLightTreshold; // Illumination definition for "twilight" in lux + @SerializedName("dark_url") + public String darkUrl; // URL to report to when luminance <= dark_threshold + @SerializedName("twilight_url") + public String twiLightUrl; // URL reports when luminance > dark_threshold AND luminance <= + @SerializedName("close_url") + public String closeUrl; // URL reports when DW contact is closed FW 1.6.5+ + @SerializedName("vibration_url") + public String vibrationUrl; // URL reports when DW detects vibration FW 1.6.5+ + + // @SerializedName("tilt_enabled") + // public Boolean tiltEnabled; // Whether tilt monitoring is activated + // @SerializedName("tilt_calibrated") + // public Boolean tiltCalibrated; // Whether calibration data is valid + // @SerializedName("vibration_enabled") + // public Boolean vibrationEnabled; // Whether vibration monitoring is activated + // @SerializedName("reverse_open_close") + // public Boolean reverseOpenClose; // Whether to reverse which position the sensor consideres "open" + } public static class ShellySettingsAttributes { @SerializedName("device_type") @@ -389,34 +539,36 @@ public static class ShellySettingsAttributes { @SerializedName("wifi_sta") public String wifiSta; // WiFi client configuration. See /settings/sta for details public String login; // credentials used for HTTP Basic authentication for the REST interface. If - // enabled is - // true clients must include an Authorization: Basic ... HTTP header with valid - // credentials - // when performing TP requests. + // enabled is true clients must include an Authorization: Basic ... HTTP header with valid + // credentials when performing TP requests. public String name; // unique name of the device. public String fw; // current FW version } public static class ShellySettingsStatus { @SerializedName("wifi_sta") - public ShellySettingsWiFiNetwork wifiSta; // WiFi client configuration. See /settings/sta for - // details + public ShellySettingsWiFiNetwork wifiSta; // WiFi client configuration. See /settings/sta for details public String time; public Integer serial; @SerializedName("has_update") public Boolean hasUpdate; public String mac; + public Boolean discoverable; // FW 1.6+ public ArrayList relays; public ArrayList rollers; public Integer input; // RGBW2 has no JSON array public ArrayList inputs; public ArrayList lights; + // @SerializedName("night_mode") // FW 1.5.7+ + // public ShellySettingsNightMode nightMode; public ArrayList dimmers; public ArrayList meters; public ArrayList emeters; - public ShellyStatusSensor.ShellySensorTmp tmp; + // Internal device temp + public ShellyStatusSensor.ShellySensorTmp tmp; // Shelly 1PM + public Double temperature; // Shelly 2.5 public Boolean overtemperature; // Shelly Dimmer only @@ -443,6 +595,8 @@ public static class ShellyControlRelay { public Boolean isValid; @SerializedName("has_timer") public Boolean hasTimer; // Whether a timer is currently armed for this channel + @SerializedName("timer_remaining") + public Integer timerRemaining; // FW 1.6+ public Boolean overpower; // Shelly1PM only if maximum allowed power was exceeded public String turn; // Accepted values are on and off. This will turn ON/OFF the respective output @@ -456,18 +610,23 @@ public static class ShellyShortStatusRelay { public Boolean ison; // Whether output channel is on or off @SerializedName("has_timer") public Boolean hasTimer; // Whether a timer is currently armed for this channel + @SerializedName("timer_remaining") + public Integer timerRemaining; public Boolean overpower; // Shelly1PM only if maximum allowed power was exceeded public Double temperature; // Internal device temperature public Boolean overtemperature; // Device over heated - - @SerializedName("ext_temperature") - public ShellyStatusSensor.ShellyExtTemperature extTemperature; // Shelly 1/1PM: sensor values } 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; // Whether a timer is currently armed for this channel + // @SerializedName("timer_remaining") + // public Integer timerRemaining; + // public Integer wgite; + // public Integer temp; // light temp } public static class ShellyStatusRelay { @@ -475,12 +634,21 @@ public static class ShellyStatusRelay { public ShellySettingsWiFiNetwork wifiSta; // WiFi status // public ShellyStatusCloud cloud; // Cloud status // public ShellyStatusMqtt mqtt; // mqtt status - public String time; // current time + public ShellySettingsCoiot coiot; // Firmware 1.6+ + // public String time; // current time public Integer serial; public String mac; // MAC public ArrayList relays; // relay status public ArrayList meters; // current meter value + @SerializedName("ext_temperature") + public ShellyStatusSensor.ShellyExtTemperature extTemperature; // Shelly 1/1PM: sensor values + @SerializedName("ext_humidity") + public ShellyStatusSensor.ShellyExtHumidity extHumidity; // Shelly 1/1PM: sensor values + + public Double temperature; // device temp acc. on the selected temp unit + public ShellyStatusSensor.ShellySensorTmp tmp; + @SerializedName("has_update") public Boolean hasUpdate; // If a newer firmware version is available public ShellySettingsUpdate update; // /status/firmware value @@ -525,17 +693,15 @@ public static class ShellyStatusDimmer { @SerializedName("fs_size") public Integer fsSize; @SerializedName("fs_free") - public Integer fsFree; // Total and available amount of file system space in - // bytes - public Integer uptime; // econds elapsed since boot + public Integer fsFree; // Total and available amount of file system space in bytes + public Integer uptime; // seconds elapsed since boot } public static class ShellyControlRoller { @SerializedName("roller_pos") public Integer rollerPos; // number Desired position in percent public Integer duration; // If specified, the motor will move for this period in seconds. If missing, the - // value of - // maxtime in /settings/roller/N will be used. + // value of maxtime in /settings/roller/N will be used. public String state; // One of stop, open, close public Double power; // Current power consumption in Watts @SerializedName("is_valid") @@ -553,21 +719,9 @@ public static class ShellyControlRoller { public Integer currentPos; // current position 0..100, 100=open } - public static final String SHELLY_STOPR_NORMAL = "normal"; - public static final String SHELLY_STOPR_SAFETYSW = "safety_switch"; - public static final String SHELLY_STOPR_OBSTACLE = "obstacle"; - - public static class ShellySettingsSensor { - @SerializedName("temperature_units") - public String temperatureUnits; // Either'C'or'F' - @SerializedName("temperature_threshold") - public Integer temperatureThreshold; // Temperature delta (in configured degree units) which triggers an update - @SerializedName("humidity_threshold") - public Integer humidityThreshold; // RH delta in % which triggers an update - @SerializedName("sleep_mode_period") - public Integer sleepModePeriod; // Periodic update period in hours, between 1 and 24 - @SerializedName("report_url") - public String reportUrl; // URL gets posted on updates with sensor data + public class ShellySensorSleepMode { + public Integer period; + public String unit; } public static class ShellyStatusSensor { @@ -590,10 +744,24 @@ public static class ShellySensorBat { public Double voltage; // battery voltage }; + // Door/Window sensor + public static class ShellySensorState { + @SerializedName("is_valid") + public Boolean isValid; // whether the internal sensor is operating properly + public String state; // Shelly Door/Window + } + public static class ShellySensorLux { @SerializedName("is_valid") public Boolean isValid; // whether the internal sensor is operating properly public Double value; + + public String illumination; + } + + public static class ShellySensorAccel { + public Integer tilt; // Tilt in ° + public Integer vibration; // Whether vibration is detected } public static class ShellyExtTemperature { @@ -612,20 +780,39 @@ public static class ShellyShortTemp { public ShellyShortTemp sensor3; } + public static class ShellyExtHumidity { + public static class ShellyShortHum { + public Double hum; // Humidity reading of sensor 0, percent + } + + // Shelly 1/1PM have up to 3 sensors + // for whatever reasons it's not an array, but 3 independent elements + @SerializedName("0") + public ShellyShortHum sensor1; + } + public ShellySensorTmp tmp; public ShellySensorHum hum; public ShellySensorLux lux; + public ShellySensorAccel accel; public ShellySensorBat bat; - + @SerializedName("sensor") + public ShellySensorState contact; + public Boolean smoke; // SHelly Smoke public Boolean flood; // Shelly Flood: true = flood condition detected @SerializedName("rain_sensor") public Boolean rainSensor; // Shelly Flood: true=in rain mode public Boolean motion; // Shelly Sense: true=motion detected public Boolean charger; // Shelly Sense: true=charger connected + @SerializedName("external_power") + public Integer externalPower; // H&T FW 1.6, seems to be the same like charger for the Sense - // @SerializedName("act_reasons") - // public String[] actReasons; // HT/Smoke/Flood: list of reasons which woke up the device + @SerializedName("act_reasons") + public String[] actReasons; // HT/Smoke/Flood: list of reasons which woke up the device + + @SerializedName("sensor_error") + public String sensorError; // 1.5.7: Only displayed in case of error } public static class ShellySettingsSmoke { @@ -637,9 +824,6 @@ public static class ShellySettingsSmoke { public Integer sleepModePeriod; // Periodic update period in hours, between 1 and 24 } - public static final String SHELLY_TEMP_CELSIUS = "C"; - public static final String SHELLY_TEMP_FAHRENHEIT = "F"; - public static class ShellySettingsLight { public Integer red; // red brightness, 0..255, applies in mode="color" public Integer green; // green brightness, 0..255, applies in mode="color" @@ -665,8 +849,14 @@ public static class ShellySettingsLight { public Boolean ison; } - public static final int SHELLY_MIN_EFFECT = 0; - public static final int SHELLY_MAX_EFFECT = 6; + public static class ShellySettingsNightMode { // FW1.5.7+ + public Integer enabled; + @SerializedName("start_time") + public String startTime; + @SerializedName("end_time") + public String endTime; + public Integer brightness; + } public static class ShellyStatusLightChannel { public Boolean ison; @@ -692,10 +882,10 @@ public static class ShellyStatusLight { public Boolean ison; // Whether output channel is on or off public ArrayList lights; public ArrayList meters; + public Integer input; // not yet used: // public String mode; // COLOR or WHITE - // public Integer input; // public Boolean has_update; // public ShellySettingsUpdate update; // public ShellySettingsWiFiNetwork wifi_sta; // WiFi client configuration. See @@ -714,44 +904,6 @@ public static class ShellySendKeyList { public ArrayList keyCodes; } - public static final String SHELLY_TIMER_AUTOON = "auto_on"; - public static final String SHELLY_TIMER_AUTOOFF = "auto_off"; - public static final String SHELLY_TIMER_ACTIVE = "has_timer"; - - public static final String SHELLY_LIGHT_TURN = "turn"; - public static final String SHELLY_LIGHT_DEFSTATE = "def_state"; - public static final String SHELLY_LIGHTTIMER = "timer"; - - public static final String SHELLY_COLOR_RED = "red"; - public static final String SHELLY_COLOR_BLUE = "blue"; - public static final String SHELLY_COLOR_GREEN = "green"; - public static final String SHELLY_COLOR_YELLOW = "yellow"; - public static final String SHELLY_COLOR_WHITE = "white"; - public static final String SHELLY_COLOR_GAIN = "gain"; - public static final String SHELLY_COLOR_BRIGHTNESS = "brightness"; - public static final String SHELLY_COLOR_TEMP = "temp"; - public static final String SHELLY_COLOR_EFFECT = "effect"; - - public static final int SHELLY_MIN_ROLLER_POS = 0; - public static final int SHELLY_MAX_ROLLER_POS = 100; - public static final int SHELLY_MIN_BRIGHTNESS = 0; - public static final int SHELLY_MAX_BRIGHTNESS = 100; - public static final int SHELLY_MIN_GAIN = 0; - public static final int SHELLY_MAX_GAIN = 100; - public static final int SHELLY_MIN_COLOR = 0; - public static final int SHELLY_MAX_COLOR = 255; - public static final int SHELLY_DIM_STEPSIZE = 10; - - // color temperature: 3000 = warm, 4750 = white, 6565 = cold; gain: 0..100 - public static final int MIN_COLOR_TEMPERATURE = 3000; - public static final int MAX_COLOR_TEMPERATURE = 6500; - public static final int COLOR_TEMPERATURE_RANGE = MAX_COLOR_TEMPERATURE - MIN_COLOR_TEMPERATURE; - public static final double MIN_BRIGHTNESS = 0.0; - public static final double MAX_BRIGHTNESS = 100.0; - public static final double SATURATION_FACTOR = 2.55; - public static final double GAIN_FACTOR = SHELLY_MAX_GAIN / 100; - public static final double BRIGHTNESS_FACTOR = SHELLY_MAX_BRIGHTNESS / 100; - /** * Shelly Dimmer returns light[]. However, the structure doesn't match the lights[] of a Bulb/RGBW2. * The tag lights[] will be replaced with dimmers[] so this could be mapped to a different Gson structure. @@ -761,10 +913,7 @@ public static class ShellySendKeyList { * @return Modified Json */ public static String fixDimmerJson(String json) { - // - // return !json.contains("\"lights\":[") ? json : json.replaceFirst(java.util.regex.Pattern.quote("\"lights\":["), "\"dimmers\":["); } - } diff --git a/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api/ShellyApiResult.java b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api/ShellyApiResult.java new file mode 100644 index 0000000000000..fbf47ddaa5076 --- /dev/null +++ b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api/ShellyApiResult.java @@ -0,0 +1,96 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.shelly.internal.api; + +import static org.eclipse.jetty.http.HttpStatus.*; +import static org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.*; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.client.api.ContentResponse; +import org.eclipse.jetty.client.api.Request; + +/** + * The {@link ShellyApiResult} wraps up the API result and provides some more information like url, http code, received + * response etc. + * + * @author Markus Michels - Initial contribution + */ +@NonNullByDefault +public class ShellyApiResult { + public String url = ""; + public String method = ""; + public String response = ""; + public int httpCode = -1; + public String httpReason = ""; + + public ShellyApiResult() { + } + + public ShellyApiResult(String method, String url) { + this.method = method; + this.url = url; + } + + public ShellyApiResult(ContentResponse contentResponse) { + fillFromResponse(contentResponse); + } + + public String getUrl() { + return !url.isEmpty() ? method + " " + url : ""; + } + + public String getHttpResponse() { + return response; + } + + @Override + public String toString() { + return getUrl() + " > " + getHttpResponse(); + } + + public boolean isHttpOk() { + return httpCode == OK_200; + } + + public boolean isHttpAccessUnauthorized() { + return (httpCode == UNAUTHORIZED_401 || response.contains(SHELLY_APIERR_UNAUTHORIZED)); + } + + public boolean isHttpTimeout() { + return httpCode == -1 || response.toUpperCase().contains(SHELLY_APIERR_TIMEOUT.toLowerCase()); + } + + public boolean isHttpServerError() { + return httpCode == INTERNAL_SERVER_ERROR_500; + } + + public boolean isNotCalibrtated() { + return getHttpResponse().contains(SHELLY_APIERR_NOT_CALIBRATED); + } + + private void fillFromResponse(@Nullable ContentResponse contentResponse) { + if (contentResponse != null) { + String r = contentResponse.getContentAsString(); + response = r != null ? r : ""; + httpCode = contentResponse.getStatus(); + httpReason = contentResponse.getReason(); + + Request request = contentResponse.getRequest(); + if (request != null) { + url = request.getURI().toString(); + method = request.getMethod(); + } + } + } +} 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 502df73a9a969..73685e2210a2f 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 @@ -12,22 +12,22 @@ */ package org.openhab.binding.shelly.internal.api; -import static org.openhab.binding.shelly.internal.ShellyBindingConstants.SHELLYDT_DIMMER; -import static org.openhab.binding.shelly.internal.ShellyUtils.*; +import static org.openhab.binding.shelly.internal.ShellyBindingConstants.*; import static org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.*; +import static org.openhab.binding.shelly.internal.util.ShellyUtils.*; import java.util.HashMap; import java.util.Map; import org.apache.commons.lang.StringUtils; -import org.apache.commons.lang.Validate; import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.shelly.internal.ShellyBindingConstants; -import org.openhab.binding.shelly.internal.ShellyUtils; import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsGlobal; +import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsStatus; +import org.openhab.binding.shelly.internal.util.ShellyUtils; import com.google.gson.Gson; +import com.google.gson.JsonSyntaxException; /** * The {@link ShellyDeviceProfile} creates a device profile based on the settings returned from the API's /settings @@ -38,14 +38,18 @@ */ @NonNullByDefault public class ShellyDeviceProfile { + public boolean initialized = false; // true when initialized + public String thingName = ""; public String deviceType = ""; public String settingsJson = ""; - public @Nullable ShellySettingsGlobal settings; + public ShellySettingsGlobal settings = new ShellySettingsGlobal(); + public ShellySettingsStatus status = new ShellySettingsStatus(); public String hostname = ""; public String mode = ""; + public boolean discoverable = true; public String hwRev = ""; public String hwBatchId = ""; @@ -54,104 +58,133 @@ public class ShellyDeviceProfile { public String fwVersion = ""; public String fwDate = ""; - public Boolean hasRelays = false; // true if it has at least 1 power meter - public Integer numRelays = 0; // number of relays/outputs - public Integer numRollers = 9; // number of Rollers, usually 1 - public Boolean isRoller = false; // true for Shelly2 in roller mode - public Boolean isDimmer = false; // true for a Shelly Dimmer (SHDM-1) - - public Boolean hasMeter = false; // true if it has at least 1 power meter - public Integer numMeters = 0; - public Boolean isEMeter = false; // true for ShellyEM - - public Boolean hasBattery = false; // true if battery device - public Boolean hasLed = false; // true if battery device - public Boolean isPlugS = false; // true if it is a Shelly Plug S - public Boolean isLight = false; // true if it is a Shelly Bulb/RGBW2 - public Boolean isBulb = false; // true pnly if it is a Bulb - public Boolean isSense = false; // true if thing is a Shelly Sense - public Boolean inColor = false; // true if bulb/rgbw2 is in color mode - public Boolean isSensor = false; // true for HT & Smoke - public Boolean isSmoke = false; // true for Smoke + public boolean hasRelays = false; // true if it has at least 1 power meter + public int numRelays = 0; // number of relays/outputs + public int numRollers = 0; // number of Rollers, usually 1 + public boolean isRoller = false; // true for Shelly2 in roller mode + public boolean isDimmer = false; // true for a Shelly Dimmer (SHDM-1) + public boolean isPlugS = false; // true if it is a Shelly Plug S + + public int numMeters = 0; + public boolean isEMeter = false; // true for ShellyEM/EM3 + + public boolean isLight = false; // true if it is a Shelly Bulb/RGBW2 + public boolean isBulb = false; // true only if it is a Bulb + public boolean isDuo = false; // true only if it is a Duo + public boolean isRGBW2 = false; // true only if it a a RGBW2 + public boolean inColor = false; // true if bulb/rgbw2 is in color mode + public boolean hasLed = false; // true if battery device + + public boolean isSensor = false; // true for HT & Smoke + public boolean hasBattery = false; // true if battery device + public boolean isSense = false; // true if thing is a Shelly Sense + public boolean isDW = false; // true of Door Window sensor + + public int minTemp = 0; // Bulb/Duo: Min Light Temp + public int maxTemp = 0; // Bulb/Duo: Max Light Temp + + public int updatePeriod = -1; public Map irCodes = new HashMap<>(); // Sense: list of stored IR codes - public Boolean supportsButtonUrls = false; // true if the btn_xxx urls are supported - public Boolean supportsOutUrls = false; // true if the out_xxx urls are supported - public Boolean supportsPushUrls = false; // true if sensor report_url is supported - public Boolean supportsRollerUrls = false; // true if the roller_xxx urls are supported - public Boolean supportsSensorUrls = false; // true if sensor report_url is supported + public ShellyDeviceProfile() { + + } - @SuppressWarnings("null") - public static ShellyDeviceProfile initialize(String thingType, String json) { + public ShellyDeviceProfile initialize(String thingType, String json) throws ShellyApiException { Gson gson = new Gson(); - ShellyDeviceProfile profile = new ShellyDeviceProfile(); - Validate.notNull(profile); + initialized = false; - profile.settingsJson = json; - profile.settings = gson.fromJson(json, ShellySettingsGlobal.class); - Validate.notNull(profile.settings, "converted device settings must not be null!"); + try { + initFromThingType(thingType); + settingsJson = json; + settings = gson.fromJson(json, ShellySettingsGlobal.class); + } catch (IllegalArgumentException | JsonSyntaxException e) { + throw new ShellyApiException(e, + thingName + ": Unable to transform settings JSON " + e.toString() + ", json='" + json + "'"); + } // General settings - profile.deviceType = ShellyUtils.getString(profile.settings.device.type); - profile.mac = getString(profile.settings.device.mac); - profile.hostname = profile.settings.device.hostname != null && !profile.settings.device.hostname.isEmpty() - ? profile.settings.device.hostname.toLowerCase() - : "shelly-" + profile.mac.toUpperCase().substring(6, 11); - profile.mode = getString(profile.settings.mode) != null ? getString(profile.settings.mode).toLowerCase() : ""; - profile.hwRev = profile.settings.hwinfo != null ? getString(profile.settings.hwinfo.hwRevision) : ""; - profile.hwBatchId = profile.settings.hwinfo != null ? getString(profile.settings.hwinfo.batchId.toString()) - : ""; - profile.fwDate = getString(StringUtils.substringBefore(profile.settings.fw, "/")); - profile.fwVersion = getString(StringUtils.substringBetween(profile.settings.fw, "/", "@")); - profile.fwId = getString(StringUtils.substringAfter(profile.settings.fw, "@")); - - profile.isRoller = profile.mode.equalsIgnoreCase(SHELLY_MODE_ROLLER); - profile.isPlugS = thingType.equalsIgnoreCase(ShellyBindingConstants.THING_TYPE_SHELLYPLUGS.getId()); - profile.hasLed = profile.isPlugS; - profile.isBulb = thingType.equalsIgnoreCase(ShellyBindingConstants.THING_TYPE_SHELLYBULB.getId()); - profile.isDimmer = profile.deviceType.equalsIgnoreCase(SHELLYDT_DIMMER); - profile.isLight = profile.isBulb - || thingType.equalsIgnoreCase(ShellyBindingConstants.THING_TYPE_SHELLYRGBW2_COLOR.getId()) - || thingType.equalsIgnoreCase(ShellyBindingConstants.THING_TYPE_SHELLYRGBW2_WHITE.getId()); - profile.inColor = profile.isLight && profile.mode.equalsIgnoreCase(SHELLY_MODE_COLOR); - - profile.isSmoke = thingType.equalsIgnoreCase(ShellyBindingConstants.THING_TYPE_SHELLYSMOKE.getId()); - profile.isSense = thingType.equalsIgnoreCase(ShellyBindingConstants.THING_TYPE_SHELLYSENSE.getId()); - profile.isSensor = profile.isSense || profile.isSmoke - || thingType.equalsIgnoreCase(ShellyBindingConstants.THING_TYPE_SHELLYHT.getId()) - || thingType.equalsIgnoreCase(ShellyBindingConstants.THING_TYPE_SHELLYFLOOD.getId()) - || thingType.equalsIgnoreCase(ShellyBindingConstants.THING_TYPE_SHELLYSENSE.getId()); - profile.hasBattery = thingType.equalsIgnoreCase(ShellyBindingConstants.THING_TYPE_SHELLYHT.getId()) - || thingType.equalsIgnoreCase(ShellyBindingConstants.THING_TYPE_SHELLYSMOKE.getId()) - || thingType.equalsIgnoreCase(ShellyBindingConstants.THING_TYPE_SHELLYFLOOD.getId()) - || thingType.equalsIgnoreCase(ShellyBindingConstants.THING_TYPE_SHELLYSENSE.getId()); - - profile.numRelays = !profile.isLight ? getInteger(profile.settings.device.numOutputs) : 0; - if ((profile.numRelays > 0) && (profile.settings.relays == null)) { - profile.numRelays = 0; + deviceType = ShellyUtils.getString(settings.device.type); + mac = getString(settings.device.mac); + hostname = settings.device.hostname != null && !settings.device.hostname.isEmpty() + ? settings.device.hostname.toLowerCase() + : "shelly-" + mac.toUpperCase().substring(6, 11); + mode = !getString(settings.mode).isEmpty() ? getString(settings.mode).toLowerCase() : ""; + hwRev = settings.hwinfo != null ? getString(settings.hwinfo.hwRevision) : ""; + hwBatchId = settings.hwinfo != null ? getString(settings.hwinfo.batchId.toString()) : ""; + fwDate = getString(StringUtils.substringBefore(settings.fw, "/")); + fwVersion = getString(StringUtils.substringBetween(settings.fw, "/", "@")); + fwId = getString(StringUtils.substringAfter(settings.fw, "@")); + discoverable = (settings.discoverable == null) || settings.discoverable; + + inColor = isLight && mode.equalsIgnoreCase(SHELLY_MODE_COLOR); + + numRelays = !isLight ? getInteger(settings.device.numOutputs) : 0; + if ((numRelays > 0) && (settings.relays == null)) { + numRelays = 0; } - profile.hasRelays = (profile.numRelays > 0) || profile.isDimmer; - profile.numRollers = getInteger(profile.settings.device.numRollers); + hasRelays = (numRelays > 0) || isDimmer; + numRollers = getInteger(settings.device.numRollers); - profile.isEMeter = profile.settings.emeters != null; - profile.numMeters = !profile.isEMeter ? getInteger(profile.settings.device.numMeters) - : getInteger(profile.settings.device.numEMeters); - if ((profile.numMeters == 0) && profile.isLight) { + isEMeter = settings.emeters != null; + numMeters = !isEMeter ? getInteger(settings.device.numMeters) : getInteger(settings.device.numEMeters); + if ((numMeters == 0) && isLight) { // RGBW2 doesn't report, but has one - profile.numMeters = profile.inColor ? 1 : getInteger(profile.settings.device.numOutputs); + numMeters = inColor ? 1 : getInteger(settings.device.numOutputs); + } + isDimmer = deviceType.equalsIgnoreCase(SHELLYDT_DIMMER); + isRoller = mode.equalsIgnoreCase(SHELLY_MODE_ROLLER); + + if (settings.sleepMode != null) { + updatePeriod = getString(settings.sleepMode.unit).equalsIgnoreCase("m") ? settings.sleepMode.period * 60 // minutes + : settings.sleepMode.period * 3600; // hours + } else if ((settings.coiot != null) && (settings.coiot.updatePeriod != null)) { + updatePeriod = 2 * getInteger(settings.coiot.updatePeriod) + 5; // usually 2*15+5s=50sec + } else { + updatePeriod = 2 * 15 + 5; // Default acc. CoIoT Spec } - profile.hasMeter = (profile.numMeters > 0); - profile.supportsButtonUrls = profile.settingsJson.contains(SHELLY_API_EVENTURL_BTN_ON) - || profile.settingsJson.contains(SHELLY_API_EVENTURL_BTN1_ON) - || profile.settingsJson.contains(SHELLY_API_EVENTURL_BTN2_ON); - profile.supportsOutUrls = profile.settingsJson.contains(SHELLY_API_EVENTURL_OUT_ON); - profile.supportsPushUrls = profile.settingsJson.contains(SHELLY_API_EVENTURL_SHORT_PUSH); - profile.supportsRollerUrls = profile.settingsJson.contains(SHELLY_API_EVENTURL_ROLLER_OPEN); - profile.supportsSensorUrls = profile.settingsJson.contains(SHELLY_API_EVENTURL_REPORT); + initialized = true; + return this; + } + + public boolean containsEventUrl(String eventType) { + return containsEventUrl(settingsJson, eventType); + } + + public boolean containsEventUrl(String json, String eventType) { + String settings = json.toLowerCase(); + return settings.contains((eventType + SHELLY_EVENTURL_SUFFIX).toLowerCase()); + } + + public boolean isInitialized() { + return initialized; + } + + public void initFromThingType(String name) { + String thingType = (name.contains("-") ? StringUtils.substringBefore(name, "-") : name).toLowerCase().trim(); + if (thingType.isEmpty()) { + return; + } - return profile; + isPlugS = thingType.equals(ShellyBindingConstants.THING_TYPE_SHELLYPLUGS_STR); + + isBulb = thingType.equals(THING_TYPE_SHELLYBULB_STR); + isDuo = thingType.equals(THING_TYPE_SHELLYDUO_STR) || thingType.equals(THING_TYPE_SHELLYVINTAGE_STR); + isRGBW2 = thingType.startsWith(THING_TYPE_SHELLYRGBW2_PREFIX); + hasLed = isPlugS; + isLight = isBulb || isDuo || isRGBW2; + minTemp = isBulb ? MIN_COLOR_TEMP_BULB : MIN_COLOR_TEMP_DUO; + maxTemp = isBulb ? MAX_COLOR_TEMP_BULB : MAX_COLOR_TEMP_DUO; + + boolean isHT = thingType.equals(THING_TYPE_SHELLYHT_STR); + boolean isFlood = thingType.equals(THING_TYPE_SHELLYFLOOD_STR); + boolean isSmoke = thingType.equals(THING_TYPE_SHELLYSMOKE_STR); + isDW = thingType.equals(THING_TYPE_SHELLYDOORWIN_STR); + isSense = thingType.equals(THING_TYPE_SHELLYSENSE_STR); + isSensor = isHT || isFlood || isDW || isSmoke || isSense; + hasBattery = isHT || isFlood || isDW || isSmoke; // we assume that Sense is connected to the charger } } 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 b880b411f649f..34c35a07e5d0d 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 @@ -16,15 +16,15 @@ import java.io.IOException; import java.nio.charset.StandardCharsets; -import java.util.HashMap; import java.util.Map; -import java.util.Scanner; +import java.util.TreeMap; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import org.apache.commons.io.IOUtils; import org.apache.commons.lang.StringUtils; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; @@ -34,8 +34,6 @@ 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.component.annotations.ReferenceCardinality; -import org.osgi.service.component.annotations.ReferencePolicy; import org.osgi.service.http.HttpService; import org.osgi.service.http.NamespaceException; import org.slf4j.Logger; @@ -54,40 +52,45 @@ public class ShellyEventServlet extends HttpServlet { private static final long serialVersionUID = 549582869577534569L; private final Logger logger = LoggerFactory.getLogger(ShellyEventServlet.class); - private @Nullable HttpService httpService; - private @Nullable ShellyHandlerFactory handlerFactory; + private final HttpService httpService; + private final ShellyHandlerFactory handlerFactory; - @SuppressWarnings("null") @Activate - protected void activate(Map config) { + public ShellyEventServlet(@Reference HttpService httpService, @Reference ShellyHandlerFactory handlerFactory, + Map config) { + this.httpService = httpService; + this.handlerFactory = handlerFactory; try { httpService.registerServlet(SHELLY_CALLBACK_URI, this, null, httpService.createDefaultHttpContext()); - logger.debug("Shelly: CallbackServlet started at '{}'", SHELLY_CALLBACK_URI); - } catch (NamespaceException | ServletException e) { - logger.warn("Shelly: Could not start CallbackServlet: {} ({})", e.getMessage(), e.getClass()); + logger.debug("ShellyEventServlet started at '{}'", SHELLY_CALLBACK_URI); + } catch (NamespaceException | ServletException | IllegalArgumentException e) { + logger.warn("Could not start CallbackServlet", e); } } @Deactivate - @SuppressWarnings("null") protected void deactivate() { - if (httpService != null) { - httpService.unregister(SHELLY_CALLBACK_URI); - } - logger.debug("Shelly: CallbackServlet stopped"); + httpService.unregister(SHELLY_CALLBACK_URI); + logger.debug("ShellyEventServlet stopped"); } - @SuppressWarnings("null") @Override protected void service(@Nullable HttpServletRequest request, @Nullable HttpServletResponse resp) - throws ServletException, IOException { - String data = inputStreamToString(request); - String path = request.getRequestURI().toLowerCase(); + throws ServletException, IOException, IllegalArgumentException { + String data = ""; + String path = ""; String deviceName = ""; String index = ""; String type = ""; + if ((request == null) || (resp == null)) { + logger.debug("request or resp must not be null!"); + return; + } + try { + path = request.getRequestURI().toLowerCase(); + data = IOUtils.toString(request.getInputStream(), "UTF-8"); String ipAddress = request.getHeader("HTTP_X_FORWARDED_FOR"); if (ipAddress == null) { ipAddress = request.getRemoteAddr(); @@ -113,52 +116,20 @@ protected void service(@Nullable HttpServletRequest request, @Nullable HttpServl index = ""; type = StringUtils.substringAfterLast(path, "/").toLowerCase(); } - logger.trace("Process event of type type={} for device {}, index={}", type, deviceName, index); - Map parms = new HashMap<>(); + logger.trace("{}: Process event of type type={}, index={}", deviceName, type, index); + Map parms = new TreeMap<>(); + for (Map.Entry p : parameters.entrySet()) { parms.put(p.getKey(), p.getValue()[0]); } - handlerFactory.onEvent(deviceName, index, type, parms); - - } catch (NullPointerException e) { - logger.debug( - "ERROR: Exception processing callback: {} ({}), path={}, data='{}'; deviceName={}, index={}, type={}, parameters={}\n{}", - e.getMessage(), e.getClass(), path, data, deviceName, index, type, - request.getParameterMap().toString(), e.getStackTrace()); + handlerFactory.onEvent(ipAddress, deviceName, index, type, parms); + } catch (IllegalArgumentException e) { + logger.debug("{}: Exception processing callback: {path={}, data='{}'; index={}, type={}, parameters={}", + deviceName, path, data, index, type, request.getParameterMap().toString()); } finally { resp.setCharacterEncoding(StandardCharsets.UTF_8.toString()); resp.getWriter().write(""); } } - - @SuppressWarnings("resource") - private String inputStreamToString(@Nullable HttpServletRequest request) throws IOException { - @SuppressWarnings("null") - Scanner scanner = new Scanner(request.getInputStream()).useDelimiter("\\A"); - return scanner.hasNext() ? scanner.next() : ""; - } - - @Reference(cardinality = ReferenceCardinality.OPTIONAL, policy = ReferencePolicy.DYNAMIC) - public void setShellyHandlerFactory(ShellyHandlerFactory handlerFactory) { - this.handlerFactory = handlerFactory; - logger.debug("Shelly Binding: HandlerFactory bound"); - } - - public void unsetShellyHandlerFactory(ShellyHandlerFactory handlerFactory) { - this.handlerFactory = null; - logger.debug("Shelly Binding: HandlerFactory unbound"); - } - - @Reference - public void setHttpService(HttpService httpService) { - this.httpService = httpService; - logger.debug("Shelly Binding: httpService bound"); - } - - public void unsetHttpService(HttpService httpService) { - this.httpService = null; - logger.debug("Shelly Binding: httpService unbound"); - } - } diff --git a/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api/ShellyHttpApi.java b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api/ShellyHttpApi.java index f93835c019931..20941dd9c2393 100644 --- a/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api/ShellyHttpApi.java +++ b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api/ShellyHttpApi.java @@ -13,22 +13,26 @@ package org.openhab.binding.shelly.internal.api; import static org.openhab.binding.shelly.internal.ShellyBindingConstants.*; -import static org.openhab.binding.shelly.internal.ShellyUtils.*; import static org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.*; +import static org.openhab.binding.shelly.internal.util.ShellyUtils.*; -import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.util.Base64; import java.util.HashMap; import java.util.Map; -import java.util.Properties; - -import javax.ws.rs.HttpMethod; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; import org.apache.commons.lang.StringUtils; -import org.apache.commons.lang.Validate; import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; -import org.eclipse.smarthome.io.net.http.HttpUtil; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.api.ContentResponse; +import org.eclipse.jetty.client.api.Request; +import org.eclipse.jetty.http.HttpHeader; +import org.eclipse.jetty.http.HttpMethod; +import org.eclipse.jetty.http.HttpStatus; +import org.eclipse.smarthome.core.library.unit.ImperialUnits; import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellyControlRoller; import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySendKeyList; import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySenseKeyCode; @@ -44,6 +48,9 @@ import org.slf4j.LoggerFactory; import com.google.gson.Gson; +import com.google.gson.JsonSyntaxException; + +import tec.uom.se.unit.Units; /** * {@link ShellyHttpApi} wraps the Shelly REST API and provides various low level function to access the device api (not @@ -53,25 +60,34 @@ */ @NonNullByDefault public class ShellyHttpApi { + public static final String HTTP_HEADER_AUTH = "Authorization"; + public static final String HTTP_AUTH_TYPE_BASIC = "Basic"; + public static final String CONTENT_TYPE_JSON = "application/json; charset=UTF-8"; + private final Logger logger = LoggerFactory.getLogger(ShellyHttpApi.class); - private final ShellyThingConfiguration config; - private final String thingName = ""; + private final HttpClient httpClient; + private ShellyThingConfiguration config = new ShellyThingConfiguration();; + private String thingName; + private final Gson gson = new Gson(); private int timeoutErrors = 0; private int timeoutsRecovered = 0; - private Gson gson = new Gson(); - private @Nullable ShellyDeviceProfile profile; + private ShellyDeviceProfile profile = new ShellyDeviceProfile(); - public ShellyHttpApi(ShellyThingConfiguration config) { - Validate.notNull(config, "Shelly Http Api: Config must not be null!"); + public ShellyHttpApi(String thingName, ShellyThingConfiguration config, HttpClient httpClient) { + this.httpClient = httpClient; + this.thingName = thingName; + setConfig(thingName, config); + profile.initFromThingType(thingName); + } + + public void setConfig(String thingName, ShellyThingConfiguration config) { + this.thingName = thingName; this.config = config; } - @Nullable - public ShellySettingsDevice getDevInfo() throws IOException { - String json = request(SHELLY_URL_DEVINFO); - logger.debug("Shelly device info : {}", json); - return gson.fromJson(json, ShellySettingsDevice.class); + public ShellySettingsDevice getDevInfo() throws ShellyApiException { + return callApi(SHELLY_URL_DEVINFO, ShellySettingsDevice.class); } /** @@ -79,25 +95,22 @@ public ShellySettingsDevice getDevInfo() throws IOException { * * @param thingType Type of DEVICE as returned from the thing properties (based on discovery) * @return Initialized ShellyDeviceProfile - * @throws IOException + * @throws ShellyApiException */ - @SuppressWarnings("null") - @Nullable - public ShellyDeviceProfile getDeviceProfile(String thingType) throws IOException { + public ShellyDeviceProfile getDeviceProfile(String thingType) throws ShellyApiException { String json = request(SHELLY_URL_SETTINGS); if (json.contains("\"type\":\"SHDM-1\"")) { - logger.trace("Detected a Shelly Dimmer: fix Json (replace lights[] tag with dimmers[]"); + logger.trace("{}: Detected a Shelly Dimmer: fix Json (replace lights[] tag with dimmers[]", thingName); json = fixDimmerJson(json); } // Map settings to device profile for Light and Sense - profile = ShellyDeviceProfile.initialize(thingType, json); - Validate.notNull(profile); + profile.initialize(thingType, json); // 2nd level initialization profile.thingName = profile.hostname; if (profile.isLight && (profile.numMeters == 0)) { - logger.debug("Get number of meters from light status"); + logger.debug("{}: Get number of meters from light status", thingName); ShellyStatusLight status = getLightStatus(); profile.numMeters = status.meters != null ? status.meters.size() : 0; } @@ -109,84 +122,79 @@ public ShellyDeviceProfile getDeviceProfile(String thingType) throws IOException return profile; } + public boolean isInitialized() { + return profile.initialized; + } + /** * Get generic device settings/status. Json returned from API will be mapped to a Gson object * * @return Device settings/status as ShellySettingsStatus object - * @throws IOException + * @throws ShellyApiException */ - @SuppressWarnings("null") - public ShellySettingsStatus getStatus() throws IOException { + public ShellySettingsStatus getStatus() throws ShellyApiException { String json = request(SHELLY_URL_STATUS); ShellySettingsStatus status = gson.fromJson(json, ShellySettingsStatus.class); - Validate.notNull(status); status.json = json; return status; } - @Nullable - public ShellyStatusRelay getRelayStatus(Integer relayIndex) throws IOException { - String result = request(SHELLY_URL_STATUS_RELEAY + "/" + relayIndex.toString()); - return gson.fromJson(result, ShellyStatusRelay.class); + public ShellyStatusRelay getRelayStatus(Integer relayIndex) throws ShellyApiException { + return callApi(SHELLY_URL_STATUS_RELEAY + "/" + relayIndex.toString(), ShellyStatusRelay.class); } - @SuppressWarnings("null") - public void setRelayTurn(Integer relayIndex, String turnMode) throws IOException { - Validate.notNull(profile); - request((!profile.isDimmer ? SHELLY_URL_CONTROL_RELEAY : SHELLY_URL_CONTROL_LIGHT) + "/" + relayIndex.toString() - + "?" + SHELLY_LIGHT_TURN + "=" + turnMode.toLowerCase()); + public ShellyShortLightStatus setRelayTurn(Integer id, String turnMode) throws ShellyApiException { + return callApi(getControlUriPrefix(id) + "?" + SHELLY_LIGHT_TURN + "=" + turnMode.toLowerCase(), + ShellyShortLightStatus.class); } - public void setDimmerBrightness(Integer relayIndex, Integer brightness, boolean autoOn) throws IOException { - if (autoOn) { - request(SHELLY_URL_CONTROL_LIGHT + "/" + relayIndex.toString() + "?" + SHELLY_LIGHT_TURN + "=" - + SHELLY_API_ON + "&brightness=" + brightness.toString()); - } else { - request(SHELLY_URL_CONTROL_LIGHT + "/" + relayIndex.toString() + "?" + "&brightness=" - + brightness.toString()); - } + public void setBrightness(Integer id, Integer brightness, boolean autoOn) throws ShellyApiException { + String turn = autoOn ? SHELLY_LIGHT_TURN + "=" + SHELLY_API_ON + "&" : ""; + request(getControlUriPrefix(id) + "?" + turn + "brightness=" + brightness.toString()); } - @Nullable - public ShellyControlRoller getRollerStatus(Integer rollerIndex) throws IOException { - String result = request(SHELLY_URL_CONTROL_ROLLER + "/" + rollerIndex.toString() + "/pos"); - return gson.fromJson(result, ShellyControlRoller.class); + public ShellyControlRoller getRollerStatus(Integer rollerIndex) throws ShellyApiException { + String uri = SHELLY_URL_CONTROL_ROLLER + "/" + rollerIndex.toString() + "/pos"; + return callApi(uri, ShellyControlRoller.class); } - public void setRollerTurn(Integer relayIndex, String turnMode) throws IOException { + public void setRollerTurn(Integer relayIndex, String turnMode) throws ShellyApiException { request(SHELLY_URL_CONTROL_ROLLER + "/" + relayIndex.toString() + "?go=" + turnMode); } - public void setRollerPos(Integer relayIndex, Integer position) throws IOException { + public void setRollerPos(Integer relayIndex, Integer position) throws ShellyApiException { request(SHELLY_URL_CONTROL_ROLLER + "/" + relayIndex.toString() + "?go=to_pos&roller_pos=" + position.toString()); } - public void setRollerTimer(Integer relayIndex, Integer timer) throws IOException { + public void setRollerTimer(Integer relayIndex, Integer timer) throws ShellyApiException { request(SHELLY_URL_CONTROL_ROLLER + "/" + relayIndex.toString() + "?timer=" + timer.toString()); } - @Nullable - public ShellyShortLightStatus getLightStatus(Integer index) throws IOException { - String result = request(SHELLY_URL_STATUS_LIGHT + "/" + index.toString()); - return gson.fromJson(result, ShellyShortLightStatus.class); + public ShellyShortLightStatus getLightStatus(Integer index) throws ShellyApiException { + return callApi(getControlUriPrefix(index), ShellyShortLightStatus.class); } - @SuppressWarnings("null") - public ShellyStatusSensor getSensorStatus() throws IOException { - Validate.notNull(profile); - ShellyStatusSensor status = gson.fromJson(request(SHELLY_URL_STATUS), ShellyStatusSensor.class); + public ShellyStatusSensor getSensorStatus() throws ShellyApiException { + ShellyStatusSensor status = callApi(SHELLY_URL_STATUS, ShellyStatusSensor.class); if (profile.isSense) { - // complete reported data - status.tmp.tC = status.tmp.units.equals(SHELLY_TEMP_CELSIUS) ? status.tmp.value : 0; - status.tmp.tF = status.tmp.units.equals(SHELLY_TEMP_FAHRENHEIT) ? status.tmp.value : 0; + // complete reported data, map C to F or vice versa: C=(F - 32) * 0.5556; + status.tmp.tC = status.tmp.units.equals(SHELLY_TEMP_CELSIUS) ? status.tmp.value + : ImperialUnits.FAHRENHEIT.getConverterTo(Units.CELSIUS).convert(getDouble(status.tmp.value)) + .doubleValue(); + status.tmp.tF = status.tmp.units.equals(SHELLY_TEMP_FAHRENHEIT) ? status.tmp.value + : Units.CELSIUS.getConverterTo(ImperialUnits.FAHRENHEIT).convert(getDouble(status.tmp.value)) + .doubleValue(); + } + if ((status.charger == null) && (status.externalPower != null)) { + // SHelly H&T uses external_power, Sense uses charger + status.charger = status.externalPower != 0; } + return status; } - @SuppressWarnings("null") - public void setTimer(Integer index, String timerName, Double value) throws IOException { - Validate.notNull(profile); + public void setTimer(Integer index, String timerName, Double value) throws ShellyApiException { String type = SHELLY_CLASS_RELAY; if (profile.isRoller) { type = SHELLY_CLASS_ROLLER; @@ -198,23 +206,19 @@ public void setTimer(Integer index, String timerName, Double value) throws IOExc request(uri); } - public void setLedStatus(String ledName, Boolean value) throws IOException { + public void setLedStatus(String ledName, Boolean value) throws ShellyApiException { request(SHELLY_URL_SETTINGS + "?" + ledName + "=" + (value ? SHELLY_API_TRUE : SHELLY_API_FALSE)); } - @Nullable - public ShellySettingsLight getLightSettings() throws IOException { - String result = request(SHELLY_URL_SETTINGS_LIGHT); - return gson.fromJson(result, ShellySettingsLight.class); + public ShellySettingsLight getLightSettings() throws ShellyApiException { + return callApi(SHELLY_URL_SETTINGS_LIGHT, ShellySettingsLight.class); } - @Nullable - public ShellyStatusLight getLightStatus() throws IOException { - String result = request(SHELLY_URL_STATUS); - return gson.fromJson(result, ShellyStatusLight.class); + public ShellyStatusLight getLightStatus() throws ShellyApiException { + return callApi(SHELLY_URL_STATUS, ShellyStatusLight.class); } - public void setLightSetting(String parm, String value) throws IOException { + public void setLightSetting(String parm, String value) throws ShellyApiException { request(SHELLY_URL_SETTINGS + "?" + parm + "=" + value); } @@ -222,10 +226,9 @@ public void setLightSetting(String parm, String value) throws IOException { * Change between White and Color Mode * * @param mode - * @throws IOException + * @throws ShellyApiException */ - @SuppressWarnings("null") - public void setLightMode(String mode) throws IOException { + public void setLightMode(String mode) throws ShellyApiException { if (!mode.isEmpty() && !profile.mode.equals(mode)) { setLightSetting(SHELLY_API_MODE, mode); profile.mode = mode; @@ -239,22 +242,16 @@ public void setLightMode(String mode) throws IOException { * @param lightIndex Index of the light, usually 0 for Bulb and 0..3 for RGBW2. * @param parm Name of the parameter (see API spec) * @param value The value - * @throws IOException + * @throws ShellyApiException */ - @SuppressWarnings("null") - public void setLightParm(Integer lightIndex, String parm, String value) throws IOException { + public void setLightParm(Integer lightIndex, String parm, String value) throws ShellyApiException { // Bulb, RGW2: //?parm?value // Dimmer: /light/?parm=value - Validate.notNull(profile); - request((!profile.isDimmer ? "/" + profile.mode : SHELLY_URL_CONTROL_LIGHT) + "/" + lightIndex.toString() + "?" - + parm + "=" + value); + request(getControlUriPrefix(lightIndex) + "?" + parm + "=" + value); } - public void setLightParms(Integer lightIndex, Map parameters) throws IOException { - Validate.notNull(profile); - @SuppressWarnings("null") - String url = (!profile.isDimmer ? "/" + profile.mode : SHELLY_URL_CONTROL_LIGHT) + "/" + lightIndex.toString() - + "?"; + public void setLightParms(Integer lightIndex, Map parameters) throws ShellyApiException { + String url = getControlUriPrefix(lightIndex) + "?"; int i = 0; for (String key : parameters.keySet()) { if (i > 0) { @@ -272,36 +269,11 @@ public void setLightParms(Integer lightIndex, Map parameters) th * map into a PRONTO code * * @return Map of key codes - * @throws IOException + * @throws ShellyApiException */ - @SuppressWarnings("null") - public Map getIRCodeList() throws IOException { + public Map getIRCodeList() throws ShellyApiException { String result = request(SHELLY_URL_LIST_IR); - - /* - * A string like this is returned with all key codes defined in the device (customizable with the App): - * String result = - * "[[\"1_231_pwr\",\"tv(231) - Power\"],[\"1_231_chdwn\",\"tv(231) - Channel Down\"],[\"1_231_chup\", - * \"tv(231) - Channel Up\"], [\"1_231_voldwn\",\"tv(231) - Volume Down\"],[\"1_231_volup\",\"tv(231) - Volume Up\"],[\"1_231_mute\" - * , - * \"tv(231) - Mute\"],[\"1_231_menu\",\"tv(231) - Menu\"],[\"1_231_inp\",\"tv(231) - Input\"],[\"1_231_info\",\"tv(231) - Info\" - * ], - * [\"1_231_left\",\"tv(231) - Left\"],[\"1_231_up\",\"tv(231) - Up\"],[\"1_231_right\",\"tv(231) - Right\"],[\"1_231_ok\" - * ,\ - * "tv(231) - OK\"],[\"1_231_down\",\"tv(231) - Down\"],[\"1_231_back\",\"tv(231) - Back\"],[\"6_546_pwr\",\"receiver(546) - Power\" - * ], - * [\"6_546_voldwn\",\"receiver(546) - Volume Down\"],[\"6_546_volup\",\"receiver(546) - Volume Up\"],[\"6_546_mute\" - * , - * \"receiver(546) - Mute\"],[\"6_546_menu\",\"receiver(546) - Menu\"],[\"6_546_info\",\"receiver(546) - Info\"],[\"6_546_left\" - * , - * \"receiver(546) - Left\"],[\"6_546_up\",\"receiver(546) - Up\"],[\"6_546_right\",\"receiver(546) - Right\"],[\"6_546_ok\" - * , - * \"receiver(546) - OK\"],[\"6_546_down\",\"receiver(546) - Down\"],[\"6_546_back\",\"receiver(546) - Back\"]]" - * ; - */ - - // take pragmatic approach to make the returned JSon into named arrays, otherwise we need to implement a - // dedicated GSonParser + // take pragmatic approach to make the returned JSon into named arrays for Gson parsing String keyList = StringUtils.substringAfter(result, "["); keyList = StringUtils.substringBeforeLast(keyList, "]"); keyList = keyList.replaceAll(java.util.regex.Pattern.quote("\",\""), "\", \"name\": \""); @@ -323,12 +295,10 @@ public Map getIRCodeList() throws IOException { * @param keyCode A keyCoud could be a symbolic name (as defined in the key map on the device) or a PRONTO Code in * plain or hex64 format * - * @throws IOException + * @throws ShellyApiException * @throws IllegalArgumentException */ - @SuppressWarnings("null") - public void sendIRKey(String keyCode) throws IOException, IllegalArgumentException { - Validate.notNull(profile); + public void sendIRKey(String keyCode) throws ShellyApiException, IllegalArgumentException { String type = ""; if (profile.irCodes.containsKey(keyCode)) { type = SHELLY_IR_CODET_STORED; @@ -341,8 +311,10 @@ public void sendIRKey(String keyCode) throws IOException, IllegalArgumentExcepti if (type.equals(SHELLY_IR_CODET_STORED)) { url = url + "&" + "id=" + keyCode; } else if (type.equals(SHELLY_IR_CODET_PRONTO)) { - String code = Base64.getEncoder().encodeToString(keyCode.getBytes()); - Validate.notNull(code, "Unable to BASE64 encode the pronto code: " + keyCode); + String code = Base64.getEncoder().encodeToString(keyCode.getBytes(StandardCharsets.UTF_8)); + if (code == null) { + throw new IllegalArgumentException("Unable to BASE64 encode the pronto code: " + keyCode); + } url = url + "&" + SHELLY_IR_CODET_PRONTO + "=" + code; } else if (type.equals(SHELLY_IR_CODET_PRONTO_HEX)) { url = url + "&" + SHELLY_IR_CODET_PRONTO_HEX + "=" + keyCode; @@ -350,27 +322,24 @@ public void sendIRKey(String keyCode) throws IOException, IllegalArgumentExcepti request(url); } - public void setSenseSetting(String setting, String value) throws IOException { + public void setSenseSetting(String setting, String value) throws ShellyApiException { request(SHELLY_URL_SETTINGS + "?" + setting + "=" + value); } /** * Set event callback URLs. Depending on the device different event types are supported. In fact all of them will be - * redirected to the binding's - * servlet and act as a trigger to schedule a status update + * redirected to the binding's servlet and act as a trigger to schedule a status update * - * @param deviceName - * @throws IOException + * @param ShellyApiException + * @throws ShellyApiException */ - public void setEventURLs() throws IOException { + public void setActionURLs() throws ShellyApiException { setRelayEvents(); setDimmerEvents(); setSensorEventUrls(); } - @SuppressWarnings("null") - private void setRelayEvents() throws IOException { - Validate.notNull(profile); + private void setRelayEvents() throws ShellyApiException { if (profile.settings.relays != null) { int num = profile.isRoller ? profile.numRollers : profile.numRelays; for (int i = 0; i < num; i++) { @@ -379,154 +348,210 @@ private void setRelayEvents() throws IOException { } } - @SuppressWarnings("null") - private void setDimmerEvents() throws IOException { - Validate.notNull(profile); + private void setDimmerEvents() throws ShellyApiException { if (profile.settings.dimmers != null) { for (int i = 0; i < profile.settings.dimmers.size(); i++) { setEventUrls(i); } + } else if (profile.isLight) { + setEventUrls(0); } } /** - * Set event URL for HT (report_url) + * Set sensor Action URLs * - * @param deviceName - * @throws IOException + * @throws ShellyApiException */ - @SuppressWarnings("null") - private void setSensorEventUrls() throws IOException { - Validate.notNull(profile); - if (profile.supportsSensorUrls && config.eventsSensorReport) { - logger.debug("Check/set Sensor Reporting URL"); - String eventUrl = "http://" + config.localIp + ":" + config.httpPort.toString() + SHELLY_CALLBACK_URI + "/" - + profile.thingName + "/" + EVENT_TYPE_SENSORDATA; - request(SHELLY_URL_SETTINGS + "?" + SHELLY_API_EVENTURL_REPORT + "=" + urlEncode(eventUrl)); + private void setSensorEventUrls() throws ShellyApiException, ShellyApiException { + if (profile.isSensor) { + logger.debug("{}: Set Sensor Reporting URL", thingName); + setEventUrl(config.eventsSensorReport, SHELLY_EVENT_SENSORREPORT, SHELLY_EVENT_DARK, SHELLY_EVENT_TWILIGHT, + SHELLY_EVENT_FLOOD_DETECTED, SHELLY_EVENT_FLOOD_GONE, SHELLY_EVENT_CLOSE, SHELLY_EVENT_VIBRATION); } } - @SuppressWarnings("null") - private void setEventUrls(Integer index) throws IOException { - Validate.notNull(profile); - String lip = config.localIp; - String localPort = config.httpPort.toString(); - String deviceName = profile.thingName; + /** + * Set/delete Relay/Roller/Dimmer Action URLs + * + * @param index Device Index (0-based) + * @throws ShellyApiException + */ + private void setEventUrls(Integer index) throws ShellyApiException { if (profile.isRoller) { - if (profile.supportsRollerUrls) { - logger.debug("Set Roller event urls"); - request(buildSetEventUrl(lip, localPort, deviceName, index, EVENT_TYPE_ROLLER, - SHELLY_API_EVENTURL_ROLLER_OPEN)); - request(buildSetEventUrl(lip, localPort, deviceName, index, EVENT_TYPE_ROLLER, - SHELLY_API_EVENTURL_ROLLER_CLOSE)); - request(buildSetEventUrl(lip, localPort, deviceName, index, EVENT_TYPE_ROLLER, - SHELLY_API_EVENTURL_ROLLER_STOP)); - } - } else { - if (profile.supportsButtonUrls && config.eventsButton) { - if (profile.settingsJson.contains(SHELLY_API_EVENTURL_BTN1_ON)) { - // 2 set of URLs, e.g. Dimmer - logger.debug("Set Dimmer event urls"); - - request(buildSetEventUrl(lip, localPort, deviceName, index, EVENT_TYPE_LIGHT, - SHELLY_API_EVENTURL_BTN1_ON)); - request(buildSetEventUrl(lip, localPort, deviceName, index, EVENT_TYPE_LIGHT, - SHELLY_API_EVENTURL_BTN1_OFF)); - request(buildSetEventUrl(lip, localPort, deviceName, index, EVENT_TYPE_LIGHT, - SHELLY_API_EVENTURL_BTN2_ON)); - request(buildSetEventUrl(lip, localPort, deviceName, index, EVENT_TYPE_LIGHT, - SHELLY_API_EVENTURL_BTN2_OFF)); - } else { - // Standard relays: btn_xxx URLs - logger.debug("Set Relay event urls"); - request(buildSetEventUrl(lip, localPort, deviceName, index, EVENT_TYPE_RELAY, - SHELLY_API_EVENTURL_BTN_ON)); - request(buildSetEventUrl(lip, localPort, deviceName, index, EVENT_TYPE_RELAY, - SHELLY_API_EVENTURL_BTN_OFF)); + setEventUrl(EVENT_TYPE_ROLLER, 0, config.eventsRoller, SHELLY_EVENT_ROLLER_OPEN, SHELLY_EVENT_ROLLER_CLOSE, + SHELLY_EVENT_ROLLER_STOP); + } else if (profile.isDimmer) { + // 2 set of URLs + setEventUrl(EVENT_TYPE_LIGHT, index, config.eventsButton, SHELLY_EVENT_BTN1_ON, SHELLY_EVENT_BTN1_OFF, + SHELLY_EVENT_BTN2_ON, SHELLY_EVENT_BTN2_OFF); + setEventUrl(EVENT_TYPE_LIGHT, index, config.eventsPush, SHELLY_EVENT_SHORTPUSH1, SHELLY_EVENT_LONGPUSH1, + SHELLY_EVENT_SHORTPUSH2, SHELLY_EVENT_LONGPUSH2); + + // Relay output + setEventUrl(EVENT_TYPE_LIGHT, index, config.eventsSwitch, SHELLY_EVENT_OUT_ON, SHELLY_EVENT_OUT_OFF); + } else if (profile.hasRelays) { + // Standard relays: btn_xxx, out_xxx, short/longpush URLs + setEventUrl(EVENT_TYPE_RELAY, index, config.eventsButton, SHELLY_EVENT_BTN_ON, SHELLY_EVENT_BTN_OFF); + setEventUrl(EVENT_TYPE_RELAY, index, config.eventsPush, SHELLY_EVENT_SHORTPUSH, SHELLY_EVENT_LONGPUSH); + setEventUrl(EVENT_TYPE_RELAY, index, config.eventsSwitch, SHELLY_EVENT_OUT_ON, SHELLY_EVENT_OUT_OFF); + } else if (profile.isLight) { + // Duo, Bulb + setEventUrl(EVENT_TYPE_LIGHT, index, config.eventsSwitch, SHELLY_EVENT_OUT_ON, SHELLY_EVENT_OUT_OFF); + } + } + + private void setEventUrl(boolean enabled, String... eventTypes) throws ShellyApiException { + for (String eventType : eventTypes) { + if (profile.containsEventUrl(eventType)) { + // Sensors add the type=xx to report_url themself, so we need to ommit here + String urlParm = !eventType.equalsIgnoreCase(SHELLY_EVENT_SENSORREPORT) ? "?type=" + eventType : ""; + String callBackUrl = "http://" + config.localIp + ":" + config.localPort + SHELLY_CALLBACK_URI + "/" + + profile.thingName + "/" + eventType + urlParm; + String newUrl = enabled ? callBackUrl : SHELLY_NULL_URL; + String test = "\"" + mkEventUrl(eventType) + "\":\"" + newUrl + "\""; + if (!enabled && !profile.settingsJson.contains(test)) { + // Don't set URL to null when the current one doesn't point to this OH + // Don't interfere with a 3rd party App + continue; + } + if (!profile.settingsJson.contains(test)) { + // Current Action URL is != new URL + request(SHELLY_URL_SETTINGS + "?" + mkEventUrl(eventType) + "=" + urlEncode(newUrl)); } } - if (profile.supportsOutUrls && config.eventsSwitch) { - request(buildSetEventUrl(lip, localPort, deviceName, index, - profile.isDimmer ? EVENT_TYPE_LIGHT : EVENT_TYPE_RELAY, SHELLY_API_EVENTURL_OUT_ON)); - request(buildSetEventUrl(lip, localPort, deviceName, index, - profile.isDimmer ? EVENT_TYPE_LIGHT : EVENT_TYPE_RELAY, SHELLY_API_EVENTURL_OUT_OFF)); - } - if (profile.supportsPushUrls && config.eventsPush) { - request(buildSetEventUrl(lip, localPort, deviceName, index, - profile.isDimmer ? EVENT_TYPE_LIGHT : EVENT_TYPE_RELAY, SHELLY_API_EVENTURL_SHORT_PUSH)); - request(buildSetEventUrl(lip, localPort, deviceName, index, - profile.isDimmer ? EVENT_TYPE_LIGHT : EVENT_TYPE_RELAY, SHELLY_API_EVENTURL_LONG_PUSH)); + } + } + + private void setEventUrl(String deviceClass, Integer index, boolean enabled, String... eventTypes) + throws ShellyApiException { + for (String eventType : eventTypes) { + if (profile.containsEventUrl(eventType)) { + if (profile.containsEventUrl(eventType)) { + String callBackUrl = "http://" + config.localIp + ":" + config.localPort + SHELLY_CALLBACK_URI + "/" + + profile.thingName + "/" + deviceClass + "/" + index + "?type=" + eventType; + String newUrl = enabled ? callBackUrl : SHELLY_NULL_URL; + String test = "\"" + mkEventUrl(eventType) + "\":\"" + callBackUrl + "\""; + if (!enabled && !profile.settingsJson.contains(test)) { + // Don't set URL to null when the current one doesn't point to this OH + // Don't interfere with a 3rd party App + continue; + } + test = "\"" + mkEventUrl(eventType) + "\":\"" + newUrl + "\""; + if (!profile.settingsJson.contains(test)) { + // Current Action URL is != new URL + logger.debug("{}: Set URL for type {} to {}", thingName, eventType, newUrl); + request(SHELLY_URL_SETTINGS + "/" + deviceClass + "/" + index + "?" + mkEventUrl(eventType) + + "=" + urlEncode(newUrl)); + } + } } } } + private static String mkEventUrl(String eventType) { + return eventType + SHELLY_EVENTURL_SUFFIX; + } + /** * Submit GET request and return response, check for invalid responses * * @param uri: URI (e.g. "/settings") */ - private String request(String uri) throws IOException { - String result = ""; - boolean retry = false; + public T callApi(String uri, Class classOfT) throws ShellyApiException { + String json = "Invalid API result"; try { - result = innerRequest(uri); - } catch (IOException e) { - String type = StringUtils.substringAfterLast(e.getCause().toString(), "."); - if (e.getMessage().contains("Timeout") || type.toLowerCase().contains("timeout") - || e.getMessage().contains("Connection reset")) { - logger.debug("{}: Shelly API timeout ({}), retry", thingName, type); - timeoutErrors++; - retry = true; - } else { - throw new IOException(thingName + ": Shelly API call failed (" + type + "), uri=" + uri); - } + json = request(uri); + return gson.fromJson(json, classOfT); + } catch (JsonSyntaxException e) { + throw new ShellyApiException(e, "Unable to convert JSON"); } - if (retry && !profile.hasBattery) { + } + + private String request(String uri) throws ShellyApiException { + ShellyApiResult apiResult = new ShellyApiResult(); + int retries = 3; + boolean timeout = false; + while (retries > 0) { try { - // retry to recover - result = innerRequest(uri); - timeoutsRecovered++; - logger.debug("Shelly API timeout recovered"); - } catch (IOException e) { - String type = StringUtils.substringAfterLast(e.getCause().toString(), "."); - if (e.getMessage().contains("Timeout") || type.toLowerCase().contains("timeout") - || e.getMessage().contains("Connection reset")) { - throw new IOException(thingName + ": Shelly API timeout (" + type + "), uri=" + uri); - } else { - throw new IOException(thingName + ": Shelly API call failed: " + type + ", uri=" + uri); + apiResult = innerRequest(HttpMethod.GET, uri); + if (timeout) { + logger.debug("{}: API timeout #{}/{} recovered ({})", thingName, timeoutErrors, timeoutsRecovered, + apiResult.getUrl()); + timeoutsRecovered++; + } + return apiResult.response; // successful + } catch (ShellyApiException e) { + retries--; + if ((!e.isTimeout() && !apiResult.isHttpServerError()) || profile.hasBattery || (retries == 0)) { + // Sensor in sleep mode or API exception for non-battery device or retry counter expired + throw e; // non-timeout exception } + + timeout = true; + timeoutErrors++; // count the retries + logger.debug("{}: API Timeout, retry #{} ({})", thingName, timeoutErrors, e.toString()); } } - return result; + throw new ShellyApiException("Inconsistent API result or Timeout"); // successful } - private String innerRequest(String uri) throws IOException { - String httpResponse = "ERROR"; + private ShellyApiResult innerRequest(HttpMethod method, String uri) throws ShellyApiException { + Request request = null; String url = "http://" + config.deviceIp + uri; - logger.trace("{}: HTTP GET for {}", thingName, url); + ShellyApiResult apiResult = new ShellyApiResult(method.toString(), url); - Properties headers = new Properties(); - if (!config.userId.isEmpty()) { - String value = config.userId + ":" + config.password; - headers.put(HTTP_HEADER_AUTH, - HTTP_AUTH_TYPE_BASIC + " " + Base64.getEncoder().encodeToString(value.getBytes())); - } + try { + request = httpClient.newRequest(url).method(method.toString()).timeout(SHELLY_API_TIMEOUT_MS, + TimeUnit.MILLISECONDS); - httpResponse = HttpUtil.executeUrl(HttpMethod.GET, url, headers, null, "", SHELLY_API_TIMEOUT_MS); - Validate.notNull(httpResponse, "httpResponse must not be null"); - // all api responses are returning the result in Json format. If we are getting - // something else it must - // be an error message, e.g. http result code - if (httpResponse.contains(APIERR_HTTP_401_UNAUTHORIZED)) { - throw new IOException( - APIERR_HTTP_401_UNAUTHORIZED + ", set/correct userid and password in the thing/binding config"); - } - if (!httpResponse.startsWith("{") && !httpResponse.startsWith("[")) { - throw new IOException("Unexpected http response: " + httpResponse); + if (!config.userId.isEmpty()) { + String value = config.userId + ":" + config.password; + request.header(HTTP_HEADER_AUTH, + HTTP_AUTH_TYPE_BASIC + " " + Base64.getEncoder().encodeToString(value.getBytes())); + } + request.header(HttpHeader.ACCEPT, CONTENT_TYPE_JSON); + logger.trace("{}: HTTP {} for {}", thingName, method, url); + + // Do request and get response + ContentResponse contentResponse = request.send(); + apiResult = new ShellyApiResult(contentResponse); + String response = contentResponse.getContentAsString().replace("\t", "").replace("\r\n", "").trim(); + logger.trace("{}: HTTP Response {}: {}", thingName, contentResponse.getStatus(), response); + + // validate response, API errors are reported as Json + if (contentResponse.getStatus() != HttpStatus.OK_200) { + throw new ShellyApiException(apiResult); + } + if (response == null || response.isEmpty() || !response.startsWith("{") && !response.startsWith("[")) { + throw new ShellyApiException("Unexpected response: " + response); + } + } catch (ExecutionException | InterruptedException | TimeoutException | IllegalArgumentException e) { + ShellyApiException ex = new ShellyApiException(apiResult, e); + if (!ex.isTimeout()) { // will be handled by the caller + logger.trace("{}: API call returned exception", thingName, ex); + } + throw ex; } + return apiResult; + } - logger.trace("HTTP response from {}: {}", thingName, httpResponse); - return httpResponse; + public String getControlUriPrefix(Integer id) { + String uri = ""; + if (profile.isLight || profile.isDimmer) { + if (profile.isDuo || profile.isDimmer) { + // Duo + Dimmer + uri = SHELLY_URL_CONTROL_LIGHT; + } else { + // Bulb + RGBW2 + uri = "/" + (profile.inColor ? SHELLY_MODE_COLOR : SHELLY_MODE_WHITE); + } + } else { + // Roller, Relay + uri = SHELLY_URL_CONTROL_RELEAY; + } + uri = uri + "/" + id; + return uri; } public int getTimeoutErrors() { diff --git a/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/coap/ShellyCoapHandler.java b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/coap/ShellyCoapHandler.java index 75400fb5a7ea9..91ccf8a1f5b44 100644 --- a/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/coap/ShellyCoapHandler.java +++ b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/coap/ShellyCoapHandler.java @@ -13,17 +13,17 @@ package org.openhab.binding.shelly.internal.coap; import static org.openhab.binding.shelly.internal.ShellyBindingConstants.*; -import static org.openhab.binding.shelly.internal.ShellyUtils.*; import static org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.*; import static org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO.*; +import static org.openhab.binding.shelly.internal.util.ShellyUtils.*; -import java.io.IOException; -import java.util.HashMap; +import java.net.UnknownHostException; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.TreeMap; import org.apache.commons.lang.StringUtils; -import org.apache.commons.lang.Validate; import org.eclipse.californium.core.CoapClient; import org.eclipse.californium.core.coap.CoAP.Code; import org.eclipse.californium.core.coap.CoAP.ResponseCode; @@ -36,9 +36,14 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.eclipse.smarthome.core.library.types.OnOffType; +import org.eclipse.smarthome.core.library.types.OpenClosedType; +import org.eclipse.smarthome.core.library.unit.ImperialUnits; import org.eclipse.smarthome.core.library.unit.SIUnits; import org.eclipse.smarthome.core.library.unit.SmartHomeUnits; +import org.eclipse.smarthome.core.thing.CommonTriggerEvents; import org.eclipse.smarthome.core.types.State; +import org.openhab.binding.shelly.internal.api.ShellyApiException; +import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsRelay; import org.openhab.binding.shelly.internal.api.ShellyDeviceProfile; import org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO.CoIotDescrBlk; import org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO.CoIotDescrSen; @@ -55,8 +60,10 @@ import com.google.gson.Gson; import com.google.gson.GsonBuilder; +import tec.uom.se.unit.Units; + /** - * The {@link ShellyCoapHandler} handles the CoIoT/Coap registration and events. + * The {@link ShellyCoapHandler} handles the CoIoT/CoAP registration and events. * * @author Markus Michels - Initial contribution */ @@ -65,63 +72,73 @@ public class ShellyCoapHandler implements ShellyCoapListener { private final Logger logger = LoggerFactory.getLogger(ShellyCoapHandler.class); private final ShellyBaseHandler thingHandler; - private final ShellyThingConfiguration config; - private final GsonBuilder gsonBuilder; + private ShellyThingConfiguration config = new ShellyThingConfiguration(); + private final GsonBuilder gsonBuilder = new GsonBuilder(); private final Gson gson; private String thingName; + private boolean discovering = false; - private @Nullable ShellyCoapServer coapServer; + private final ShellyCoapServer coapServer; private @Nullable CoapClient statusClient; - private @Nullable Request reqDescription; - private @Nullable Request reqStatus; + private Request reqDescription = new Request(Code.GET, Type.CON); + private Request reqStatus = new Request(Code.GET, Type.CON); private int lastSerial = -1; private String lastPayload = ""; - private Map blockMap = new HashMap<>(); - private Map sensorMap = new HashMap<>(); + private Map blockMap = new LinkedHashMap<>(); + private LinkedHashMap sensorMap = new LinkedHashMap<>(); + + private static final byte[] EMPTY_BYTE = new byte[0]; - public ShellyCoapHandler(ShellyThingConfiguration config, ShellyBaseHandler thingHandler, - @Nullable ShellyCoapServer coapServer) { - Validate.notNull(coapServer); + public ShellyCoapHandler(ShellyBaseHandler thingHandler, ShellyCoapServer coapServer) { this.thingHandler = thingHandler; this.coapServer = coapServer; - this.config = config; this.thingName = thingHandler.thingName; - gsonBuilder = new GsonBuilder(); gsonBuilder.registerTypeAdapter(CoIotGenericSensorList.class, new CoIotSensorTypeAdapter()); gsonBuilder.setPrettyPrinting(); gson = gsonBuilder.create(); } - /* - * Initialize Coap access, send discovery packet and start Status server + /** + * Initialize CoAP access, send discovery packet and start Status server + * + * @parm thingName Thing name derived from Thing Type/hostname + * @parm config ShellyThingConfiguration + * @thows ShellyApiException */ - @SuppressWarnings("null") - public void start() { + public synchronized void start(String thingName, ShellyThingConfiguration config) throws ShellyApiException { + if (isStarted()) { + logger.trace("{}: CoAP Listener was already started", thingName); + return; + } try { + this.thingName = thingName; + this.config = config; + reqDescription = sendRequest(reqDescription, config.deviceIp, COLOIT_URI_DEVDESC, Type.CON); - if (statusClient == null) { - coapServer.init(config.localIp); - coapServer.addListener(this); + if (!isStarted()) { + logger.debug("{}: Starting CoAP Listener", thingName); + reqDescription = sendRequest(reqDescription, config.deviceIp, COLOIT_URI_DEVDESC, Type.CON); + coapServer.start(config.localIp, this); statusClient = new CoapClient(completeUrl(config.deviceIp, COLOIT_URI_DEVSTATUS)) .setTimeout((long) SHELLY_API_TIMEOUT_MS).useNONs().setEndpoint(coapServer.getEndpoint()); - - coapServer.start(); } - } catch (IOException e) { - logger.warn("{}: Unable to start CoIoT: {}", thingName, e.getMessage()); - } catch (NullPointerException e) { - logger.debug("{}: Coap Exception: {} ({})\n{}", thingName, e.getMessage(), e.getClass(), e.getStackTrace()); + } catch (UnknownHostException e) { + ShellyApiException ea = new ShellyApiException(e); + logger.debug("{}: CoAP Exception", thingName, e); + throw ea; } } + public boolean isStarted() { + return statusClient != null; + } + /** - * Process an inbound Response (or mapped Request) - * - decode Coap options - * - handle discery result or status updates + * Process an inbound Response (or mapped Request): decode CoAP options. handle discovery result or status updates * * @param response The Response packet */ @@ -141,8 +158,10 @@ public void processResponse(@Nullable Response response) { // int validity = 0; int serial = 0; try { - logger.debug("{}: CoIoT Message from {}: {}", thingName, response.getSourceContext().getPeerAddress(), - response.toString()); + if (logger.isDebugEnabled()) { + logger.debug("{}: CoIoT Message from {} (MID={}): {}", thingName, + response.getSourceContext().getPeerAddress(), response.getMID(), response.getPayloadString()); + } if (response.isCanceled() || response.isDuplicate() || response.isRejected()) { logger.debug("{} ({}): Packet was canceled, rejected or is a duplicate -> discard", thingName, devId); return; @@ -151,12 +170,9 @@ public void processResponse(@Nullable Response response) { if (response.getCode() == ResponseCode.CONTENT) { payload = response.getPayloadString(); List