From a6d1a702f508d45364f5ce20fb6a321422f5bc69 Mon Sep 17 00:00:00 2001 From: Markus Michels Date: Wed, 31 Mar 2021 22:42:33 +0200 Subject: [PATCH] [shelly] New Shelly Manager (more detailed information, status, integrated firmware upgrade) (#10276) * This PR brings in the Shelly Manager, check doc/ShellyManager.md for additional information. * Restart Device in Manager when CoIoT Mode has changed * Updated pattern to extract version info (thanks @fwolter), CoIoT warning for non-Motion devices fixed; AdvancedUsers.md now refers to Shelly Manager * Modified message when beta is detected (reference to 1.5.7 release build confuses users running 1.10 beta) * Fix for Enable/Disable AP roaming * Handle button events also in detached mode, README updated * Ignore inconsistent version string for initial 1.10 releases * removed display of firmware id (there are various formats and it has no value) Signed-off-by: Markus Michels --- bundles/org.openhab.binding.shelly/README.md | 27 +- .../doc/ShellyManager.md | 186 ++++++ .../internal/ShellyBindingConstants.java | 2 +- .../shelly/internal/api/ShellyApiJsonDTO.java | 14 +- .../shelly/internal/api/ShellyApiResult.java | 4 + .../internal/api/ShellyDeviceProfile.java | 18 +- .../shelly/internal/api/ShellyHttpApi.java | 40 +- .../internal/coap/ShellyCoIoTProtocol.java | 3 + .../internal/coap/ShellyCoapHandler.java | 25 +- .../internal/handler/ShellyBaseHandler.java | 39 +- .../internal/manager/ShellyManager.java | 65 ++ .../manager/ShellyManagerActionPage.java | 356 +++++++++++ .../internal/manager/ShellyManagerCache.java | 100 +++ .../manager/ShellyManagerConstants.java | 149 +++++ .../manager/ShellyManagerImageLoader.java | 70 +++ .../manager/ShellyManagerOtaPage.java | 221 +++++++ .../manager/ShellyManagerOverviewPage.java | 307 +++++++++ .../internal/manager/ShellyManagerPage.java | 591 ++++++++++++++++++ .../manager/ShellyManagerServlet.java | 150 +++++ .../resources/OH-INF/i18n/shelly.properties | 65 +- .../OH-INF/i18n/shelly_de.properties | 4 +- .../main/resources/images/cloud_connected.png | Bin 0 -> 282 bytes .../main/resources/images/cloud_disabled.png | Bin 0 -> 4636 bytes .../main/resources/images/cloud_enabled.png | Bin 0 -> 4533 bytes .../main/resources/images/cloud_unknown.png | Bin 0 -> 4027 bytes .../main/resources/images/mqtt_connected.png | Bin 0 -> 603 bytes .../main/resources/images/mqtt_disabled.png | Bin 0 -> 554 bytes .../main/resources/images/mqtt_enabled.png | Bin 0 -> 566 bytes .../main/resources/images/mqtt_unknown.png | Bin 0 -> 4027 bytes .../src/main/resources/images/otacheck.png | Bin 0 -> 4600 bytes .../src/main/resources/images/refresh.png | Bin 0 -> 1877 bytes .../src/main/resources/images/resetstat.png | Bin 0 -> 4724 bytes .../resources/images/status_attention.png | Bin 0 -> 2517 bytes .../main/resources/images/status_config.png | Bin 0 -> 2472 bytes .../resources/images/status_initializing.png | Bin 0 -> 1918 bytes .../main/resources/images/status_offline.png | Bin 0 -> 1950 bytes .../main/resources/images/status_online.png | Bin 0 -> 1934 bytes .../resources/images/status_uninitialized.png | Bin 0 -> 1687 bytes .../main/resources/images/status_unknown.png | Bin 0 -> 1945 bytes .../src/main/resources/images/wifi.png | Bin 0 -> 4027 bytes .../src/main/resources/images/wifi0.png | Bin 0 -> 3924 bytes .../src/main/resources/images/wifi1.png | Bin 0 -> 3955 bytes .../src/main/resources/images/wifi2.png | Bin 0 -> 4068 bytes .../src/main/resources/images/wifi3.png | Bin 0 -> 4198 bytes .../src/main/resources/images/wifi4.png | Bin 0 -> 4280 bytes .../src/main/resources/sniplets/action.html | 30 + .../src/main/resources/sniplets/footer.html | 13 + .../main/resources/sniplets/forward.script | 3 + .../main/resources/sniplets/fw_update1.html | 50 ++ .../main/resources/sniplets/fw_update2.html | 17 + .../src/main/resources/sniplets/header.html | 43 ++ .../main/resources/sniplets/ov_device.html | 106 ++++ .../main/resources/sniplets/ov_footer.html | 33 + .../main/resources/sniplets/ov_header.html | 37 ++ .../src/main/resources/sniplets/overview.html | 60 ++ 55 files changed, 2790 insertions(+), 38 deletions(-) create mode 100644 bundles/org.openhab.binding.shelly/doc/ShellyManager.md create mode 100644 bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/manager/ShellyManager.java create mode 100644 bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/manager/ShellyManagerActionPage.java create mode 100644 bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/manager/ShellyManagerCache.java create mode 100644 bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/manager/ShellyManagerConstants.java create mode 100644 bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/manager/ShellyManagerImageLoader.java create mode 100644 bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/manager/ShellyManagerOtaPage.java create mode 100644 bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/manager/ShellyManagerOverviewPage.java create mode 100644 bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/manager/ShellyManagerPage.java create mode 100644 bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/manager/ShellyManagerServlet.java create mode 100644 bundles/org.openhab.binding.shelly/src/main/resources/images/cloud_connected.png create mode 100644 bundles/org.openhab.binding.shelly/src/main/resources/images/cloud_disabled.png create mode 100644 bundles/org.openhab.binding.shelly/src/main/resources/images/cloud_enabled.png create mode 100644 bundles/org.openhab.binding.shelly/src/main/resources/images/cloud_unknown.png create mode 100644 bundles/org.openhab.binding.shelly/src/main/resources/images/mqtt_connected.png create mode 100644 bundles/org.openhab.binding.shelly/src/main/resources/images/mqtt_disabled.png create mode 100644 bundles/org.openhab.binding.shelly/src/main/resources/images/mqtt_enabled.png create mode 100644 bundles/org.openhab.binding.shelly/src/main/resources/images/mqtt_unknown.png create mode 100644 bundles/org.openhab.binding.shelly/src/main/resources/images/otacheck.png create mode 100644 bundles/org.openhab.binding.shelly/src/main/resources/images/refresh.png create mode 100644 bundles/org.openhab.binding.shelly/src/main/resources/images/resetstat.png create mode 100644 bundles/org.openhab.binding.shelly/src/main/resources/images/status_attention.png create mode 100644 bundles/org.openhab.binding.shelly/src/main/resources/images/status_config.png create mode 100644 bundles/org.openhab.binding.shelly/src/main/resources/images/status_initializing.png create mode 100644 bundles/org.openhab.binding.shelly/src/main/resources/images/status_offline.png create mode 100644 bundles/org.openhab.binding.shelly/src/main/resources/images/status_online.png create mode 100644 bundles/org.openhab.binding.shelly/src/main/resources/images/status_uninitialized.png create mode 100644 bundles/org.openhab.binding.shelly/src/main/resources/images/status_unknown.png create mode 100644 bundles/org.openhab.binding.shelly/src/main/resources/images/wifi.png create mode 100644 bundles/org.openhab.binding.shelly/src/main/resources/images/wifi0.png create mode 100644 bundles/org.openhab.binding.shelly/src/main/resources/images/wifi1.png create mode 100644 bundles/org.openhab.binding.shelly/src/main/resources/images/wifi2.png create mode 100644 bundles/org.openhab.binding.shelly/src/main/resources/images/wifi3.png create mode 100644 bundles/org.openhab.binding.shelly/src/main/resources/images/wifi4.png create mode 100644 bundles/org.openhab.binding.shelly/src/main/resources/sniplets/action.html create mode 100644 bundles/org.openhab.binding.shelly/src/main/resources/sniplets/footer.html create mode 100644 bundles/org.openhab.binding.shelly/src/main/resources/sniplets/forward.script create mode 100644 bundles/org.openhab.binding.shelly/src/main/resources/sniplets/fw_update1.html create mode 100644 bundles/org.openhab.binding.shelly/src/main/resources/sniplets/fw_update2.html create mode 100644 bundles/org.openhab.binding.shelly/src/main/resources/sniplets/header.html create mode 100644 bundles/org.openhab.binding.shelly/src/main/resources/sniplets/ov_device.html create mode 100644 bundles/org.openhab.binding.shelly/src/main/resources/sniplets/ov_footer.html create mode 100644 bundles/org.openhab.binding.shelly/src/main/resources/sniplets/ov_header.html create mode 100644 bundles/org.openhab.binding.shelly/src/main/resources/sniplets/overview.html diff --git a/bundles/org.openhab.binding.shelly/README.md b/bundles/org.openhab.binding.shelly/README.md index dee49a1585f33..459036cb55aa1 100644 --- a/bundles/org.openhab.binding.shelly/README.md +++ b/bundles/org.openhab.binding.shelly/README.md @@ -16,6 +16,14 @@ The binding gets in sync with the next status refresh. Refer to [Advanced Users](doc/AdvancedUsers.md) for more information on openHAB Shelly integration, e.g. firmware update, network communication or log filtering. +Also check out the [Shelly Manager](doc/ShellyManager.md), which +- provides detailed information on your Shellys +- helps to diagnose WiFi issues or device instabilities +- includes some common actions and +- simplifies firmware updates. + +[Shelly Manager](doc/ShellyManager.md) could also act as a firmware upgrade proxy - the device doesn't need to connect directly to the Internet, instead openHAB services as a download proxy, which improves device security. + ## Supported Devices | thing-type | Model | Vendor ID | @@ -257,16 +265,20 @@ The following trigger types are sent: |Event Type |Description | |-------------------|---------------------------------------------------------------------------------------------------------------| -|SHORT_PRESSED |The button was pressed once for a short time | -|DOUBLE_PRESSED |The button was pressed twice with short delay | -|TRIPLE_PRESSED |The button was pressed three times with short delay | -|LONG_PRESSED |The button was pressed for a longer time | -|SHORT_LONG_PRESSED |A short followed by a long button push | -|LONG_SHORT_PRESSED |A long followed by a short button push | +|SHORT_PRESSED |The button was pressed once for a short time (lastEvent=S) | +|DOUBLE_PRESSED |The button was pressed twice with short delay (lastEvent=SS) | +|TRIPLE_PRESSED |The button was pressed three times with short delay (lastEvent=SSS) | +|LONG_PRESSED |The button was pressed for a longer time (lastEvent=L) | +|SHORT_LONG_PRESSED |A short followed by a long button push (lastEvent=SL) | +|LONG_SHORT_PRESSED |A long followed by a short button push (lastEvent=LS) | Check the channel definitions for the various devices to see if the device supports those events. You could use the Shelly App to set the timing for those events. +If you want to use those events triggering a rule: +- If a physical switch is connected to the Shelly use the input channel(`input` or `input1`/`input2`) to trigger a rule +- For a momentary button use the `button` trigger channel as trigger, channels `lastEvent` and `eventCount` will provide details on the event + ### Alarms The binding provides health monitoring functions for the device. @@ -796,7 +808,8 @@ You can define 2 items (1 Switch, 1 Number) mapping to the same channel, see exa Important: The Shelly Motion does only support CoIoT Unicast, which means you need to set the CoIoT peer address. -Use device WebUI, open COIOT settings, make sure CoIoT is enabled and enter the openHAB IP address or +- Use device WebUI, open COIOT settings, make sure CoIoT is enabled and enter the openHAB IP address or +- Use [Shelly Manager](doc/ShellyManager.md, select Action 'Set CoIoT peer' and the Manager will sets the openHAB IP address as peer address |Group |Channel |Type |read-only|Description | |----------|---------------|---------|---------|---------------------------------------------------------------------| diff --git a/bundles/org.openhab.binding.shelly/doc/ShellyManager.md b/bundles/org.openhab.binding.shelly/doc/ShellyManager.md new file mode 100644 index 0000000000000..909d373528700 --- /dev/null +++ b/bundles/org.openhab.binding.shelly/doc/ShellyManager.md @@ -0,0 +1,186 @@ +# Shelly Manager + +The Shelly Manager is a small extension to the binding, which provides some low level information on the Shelly Devices, but also provides some functions to manage the devices. + +To open the Shelly Manage launch the following URL in your browser +- http://<openHAB IP address>:8080/shelly/manager or +- http://<openHAB IP address>:8443/shelly/manager + +Maybe you need to change the port matching your setup. + +Shelly Manager makes you various device insights available to get an overview of your Shellys +- Get a quick overview that all Shellys operate like expected, statistical data will help to identify issues +- Have some basic setting actions integrated, which help to do an easy setup of new Shellys added to openHAB +- Make firmware updates way easier - filter 'Update available' + integrated 2-click update +- Provide a firmware download proxy, which allows to separate your Shellys from the Internet (improved device security) + +## Overview + +Once the Shelly Manager is opened an overview of all Shelly devices added as a Thing are displayed. +Things which are not discovered or still site in the Inbox will not be displayed. + +![](images/manager/overview.png) + +You'll see a bunch of technical details, which are not available as channels or in the Thing properties. +This includes information on the device communication stability. +The statistic gives you a good overview if device communication is stable or a relevant number of timeouts need to be recovered. +In this case you should verify the WiFi coverage or other options to improve stability. + +The following information is available +|Column |Description | +|--------------------|---------------------------------------------------------------------------------| +|S |Thing Status - hover over the icon to see more details | +|Name |Device name - hover over the name to get more details | +|Cloud Status Icon |Indicates the status of the Shelly Cloud feature: disabled/enabled/connected | +|MQQT Status Icon |Indicates the staus of the MQTT featured disabled/enabled/connected | +|Refresh button |Trigger a status refresh in background, maybe you need to click more than once | +|Device IP |Assigned IP address, click to open the device’s Web UI in a separate browser tab | +|WiFi Network |SSID of the connected WiFi network | +|WiFi Signal |WiFi signal strength, 0=none, 4=very good | +|Battery Level |Remaining capacity of the battery | +|Heartbeat |Last time a response or an event was received from the device | +|Actions |Drop down with some actions, see below | +|Firmware |Current firmware release | +|Update avail |yes indicates that a firmware update is available | +|Versions |List available firmware versions: prod, beta or archived | +|Uptime |Number of seconds since last device restart | +|Internal Temp |Device internal temperature. Max is depending on device type. | +|Update Period |Timeout for device refresh | +|Remaining Watchdog |Shows number of seconds until device will go offline if no update is received | +|Events |Increases on every event triggered by the device or the binding | +|Last Event |Type of last event or alarm (refer README.md for details) | +|Event Time |When was last event received | +|Device Restarts |Number of detected restarts. This is ok on firmware updates, otherwise indicates a crash | +|Timeout Errors |Number of API timeouts, could be an indication for an unstable connection | +|Timeouts recovered |The binding does retries and timeouts and counts successful recoveries | +|CoIoT Messages |Number of received CoIoT messages, must be >= 2 to indicate CoIoT working | +|CoIoT Errors |Number of CoIoT messages, which can't be processed. >0 indicates firmware issues | + +The column S and Name display more information when hovering with the mouse over the entries. + +![](images/manager/overview_devstatus.png) +![](images/manager/overview_devsettings.png) + +### Device Filters +|Filter |Description | +|--------------------|---------------------------------------------------------------------------------| +|All |Clear filter / display all devices | +|Online only |Filter on devices with Thing Status = ONLINE | +|Inactive only |Filter on devices, which are not initialized for in Thing Status = OFFLINE | +|Needs Attention |Filter on devices, which need attention (setup/connectivity issues), see below | +|Update available |Filter on devices having a new firmware version available | +|Unprotected |Filter on devices, which are currently not password protected | + +Beside the Device Filter box you see a refresh button. +At the bottom right you see number of displayed devices vs. number of total devices. +A click triggers a background status update for all devices rather only the selected one when clicking of the refresh button in the device lines. + +Filter 'Needs Attention': +This is a dynamic filter, which helps to identify devices having some kind of setup / connectivity or operation issues. +The binding checks the following conditions +- Thing status != ONLINE: Use the 'Inactive Only' filter to find those devices, check openhab.log +- WIFISIGNAL: WiFi signal strength < 2 - this usually leads into connectivity problems, check positioning of portable devices or antenna direction. +- LOWBATTERY: The remaining battery is < 20% (configuration in Thing Configuration), consider to replace the battery +Watch out for bigger number of timeout errors. +- Device RESTARTED: Indicates a firmware problem / crash if this happens without a device reboot or firmware update (timestamp is included) +- OVERTEMP / OVERLOAD / LOADERROR: There are problems with the physical installation of the device, check specifications, wiring, housing! +- SENSORERROR: A sensor error / malfunction was detected, check product documentation +- NO_COIOT_DISCOVERY: The CoIoT discovery has not been completed, check IP network configuration, re-discover the device +- NO_COIOT_MULTICAST: The CoIoT discovery could be completed, but the device is not receiving CoIoT status updates. +You might try to switch to CoIoT Peer mode, in this case the device doesn't use IP Multicast and sends updates directly to the openHAB host. + +The result is shown in the Device Status tooltip. + +### Device settings & status + +When hovering with the mouse over the status icon or the device name you'll get additional information settings and status. + +### Device Status + +|Status |Description | +|--------------------|---------------------------------------------------------------------------------| +|Status |Thing status, sub-status and description as you know it from openHAB | +|CoIoT Status |CoIoT status: enabled or disabled | +|CoIoT Destination |CoIoT Peer address (ip address:port) or Multicast | +|Cloud Status |Status of the Shelly Cloud connection: disabled, enabled, connected | +|MQTT Status |MQTT Status: disabled, enabled, connected | +|Actions skipped |Number of actions skipped by the device, usually 0 | +|Max Internal Temp |Maximum internal temperature, check device specification for valid range | + +### Device Settings + +|Setting |Description | +|--------------------|---------------------------------------------------------------------------------| +|Shelly Device Name |Device name according to device settings | +|Device Hardware Rev |Hardware revision of the device | +|Device Type |Device Type ID | +|Device Mode |Selected mode for dual mode devices (relay/roller or white/color) | +|Firmware Version |Current firmware version | +|Network Name |Network name of the device used for mDNS | +|MAC Address |Unique hardware/network address of the device | +|Discoverable |true: the device can be discovered using mDNS, false: device is hidden | +|WiFi Auto Recovery |enabled: the device will automatically reboot when WiFi connect fails | +|Timezone |Configured device zone (see device settings) | +|Time server |Configured time server (use device UI to change) | + +### Actions + +The Shelly Manager provides the following actions when the Thing is ONLINE. +They are available in the dropdown list in column Actions. + +|Action |Description | +|---------------------|---------------------------------------------------------------------------------| +|Reset Statistics |Resets device statistic and clear the last alarm | +|Restart |Restart the device and reconnect to WiFi | +|Protect |Use binding's default credentials to protect device access with user and password| +|Set CoIoT Peer |Disable CoIoT Multicast and set openHAB system as receiver for CoIoT updates | +|Set CoIoT Multicast |Disable CoIoT Multicast and set openHAB system as receiver for CoIoT updates | +|Enable Cloud |Enable the Shelly Cloud connectivity | +|Disable Cloud |Disable the Shelly Cloud connectivity (takes about 15sec to become active) | +|Reconnect WiFi |Sensor devices only: Clears the STA/AP list and reconnects to strongest AP | +|Enable WiFi Roaming |The device will connect to the strongest AP when roadming is enabled | +|Disable WiFi Roaming |Disable Access Point Roaming, device will periodically search for better APs | +|Enable WiFi Recovery |Enables auto-restart if device detects persistent WiFi connectivity issues | +|Disable WiFi Recovery|Disables device auto-restart ion persistent WiFi connectivity issues | +|Factory Reset |Performs a **factory reset**; Attention: The device will lose its configuration | +|Enable Device Debug |Enables on-device debug log - activate only when requested by Allterco support | +|Get Debug Log |Retrieve and display device debug output | +|Get Debug Log1 |Retrieve and display 2nd device debug output | +|Factory Reset |Performs **firmware reset**; Attention: The device will lose its configuration | + +Note: Various actions available only for certain devices or when using a minimum firmware version. + +![](images/manager/overview_actions.png) + +## Firmware Update + +The Shelly Manager simplifies the firmware update. +You could select between different versions using the drop down list on the overview page. + +Shelly Manager integrates different sources +- Allterco official releases: production and beta release (like in the device UI) +- Older firmware release from the firmware archive - this is a community service +- You could specify any custom URL providing the firmware image (e.g. a local web server), which is accessible for the device using http + +| | | +|-|-| +|![](images/manager/overview_versions.png)|All firmware releases are combined to the selection list.
Click on the version you want to install and Shelly Manager will generate the requested URL to trigger the firmware upgrade.| + +The upgrade starts if you click "Perform Update". + +![](images/manager/fwupgrade.png) + +The device will download the firmware file, installs the update and restarts the device. +Depending on the device type this takes between 10 and 60 seconds. +The binding will automatically recover the device with the next status check (as usual). + +### Connection types + +You could choose between 3 different update types +* Internet: This triggers the regular update; the device needs to be connected to the Internet +* Use openHAB as a proxy: In this case the binding directs the device to request the firmware from the openHAB system. +The binding will then download the firmware from the selected sources and passes this transparently to the device. +This provides a security benefit: The device doesn't require Internet access, only the openHAB host, which could be filtered centrally. +* Custom URL: In this case you could specify + +The binding manages the download request with the proper download URL. 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 adb8ab68d76e9..09ca61a5301ec 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 @@ -194,7 +194,6 @@ public class ShellyBindingConstants { 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"; @@ -327,6 +326,7 @@ public class ShellyBindingConstants { public static final String SHELLY_API_MIN_FWVERSION = "v1.5.7";// v1.5.7+ public static final String SHELLY_API_MIN_FWCOIOT = "v1.6";// v1.6.0+ public static final String SHELLY_API_FWCOIOT2 = "v1.8";// CoAP 2 with FW 1.8+ + public static final String SHELLY_API_FW_110 = "v1.10"; // FW 1.10 or newer detected, activates some add feature // Alarm types/messages public static final String ALARM_TYPE_NONE = "NONE"; 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 d4be3688a9d26..ca1d1379a93a4 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 @@ -543,7 +543,9 @@ public static class ShellySettingsGlobal { @SerializedName("wifi_sta1") public ShellySettingsWiFiNetwork wifiSta1; @SerializedName("wifirecovery_reboot_enabled") - public Boolean wifiRecoveryReboot; + public Boolean wifiRecoveryReboot; // FW 1.10+ + @SerializedName("ap_roaming") + public ShellyApRoaming apRoaming; // FW 1.10+ public ShellySettingsMqtt mqtt; // not used for now public ShellySettingsSntp sntp; // not used for now @@ -563,6 +565,7 @@ public static class ShellySettingsGlobal { public ShellySensorSleepMode sleepMode; // FW 1.6 @SerializedName("external_power") public Integer externalPower; // H&T FW 1.6, seems to be the same like charger for the Sense + public Boolean debug_enable; // FW 1.10+ public String timezone; public Double lat; @@ -891,6 +894,15 @@ public static class ShellyControlRoller { public Integer currentPos; // current position 0..100, 100=open } + public class ShellyOtaCheckResult { + public String status; + } + + public class ShellyApRoaming { + public Boolean enabled; + public Integer threshold; + } + public class ShellySensorSleepMode { public Integer period; public String unit; 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 index b1a2bc143098d..4cadd3f96af20 100644 --- 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 @@ -63,6 +63,10 @@ public boolean isHttpOk() { return httpCode == OK_200; } + public boolean isNotFound() { + return httpCode == NOT_FOUND_404; + } + public boolean isHttpAccessUnauthorized() { return (httpCode == UNAUTHORIZED_401 || response.contains(SHELLY_APIERR_UNAUTHORIZED)); } 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 2d4a832e6e316..45cc461cf527c 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 @@ -29,6 +29,7 @@ import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsRelay; import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsRgbwLight; import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsStatus; +import org.openhab.binding.shelly.internal.util.ShellyVersionDTO; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -44,12 +45,13 @@ @NonNullByDefault public class ShellyDeviceProfile { private final Logger logger = LoggerFactory.getLogger(ShellyDeviceProfile.class); - private final static Pattern VERSION_PATTERN = Pattern.compile("v\\d+\\.\\d+\\.\\d+"); + private final static Pattern VERSION_PATTERN = Pattern.compile("v\\d+\\.\\d+\\.\\d+(-[a-z0-9]*)?"); public boolean initialized = false; // true when initialized public String thingName = ""; public String deviceType = ""; + public boolean extFeatures = false; public String settingsJson = ""; public ShellySettingsGlobal settings = new ShellySettingsGlobal(); @@ -64,7 +66,6 @@ public class ShellyDeviceProfile { public String hwRev = ""; public String hwBatchId = ""; public String mac = ""; - public String fwId = ""; public String fwVersion = ""; public String fwDate = ""; @@ -126,7 +127,8 @@ public ShellyDeviceProfile initialize(String thingType, String json) throws Shel hwBatchId = settings.hwinfo != null ? getString(settings.hwinfo.batchId.toString()) : ""; fwDate = substringBefore(settings.fw, "/"); fwVersion = extractFwVersion(settings.fw); - fwId = substringAfter(settings.fw, "@"); + ShellyVersionDTO version = new ShellyVersionDTO(); + extFeatures = version.compare(fwVersion, SHELLY_API_FW_110) >= 0; discoverable = (settings.discoverable == null) || settings.discoverable; inColor = isLight && mode.equalsIgnoreCase(SHELLY_MODE_COLOR); @@ -314,7 +316,8 @@ public boolean inButtonMode(int idx) { logger.trace("{}: Checking for trigger, button-type[{}] is {}", thingName, idx, btnType); return btnType.equalsIgnoreCase(SHELLY_BTNT_MOMENTARY) || btnType.equalsIgnoreCase(SHELLY_BTNT_MOM_ON_RELEASE) - || btnType.equalsIgnoreCase(SHELLY_BTNT_ONE_BUTTON) || btnType.equalsIgnoreCase(SHELLY_BTNT_TWO_BUTTON); + || btnType.equalsIgnoreCase(SHELLY_BTNT_ONE_BUTTON) || btnType.equalsIgnoreCase(SHELLY_BTNT_TWO_BUTTON) + || btnType.equalsIgnoreCase(SHELLY_BTNT_DETACHED); } public int getRollerFav(int id) { @@ -327,9 +330,12 @@ public int getRollerFav(int id) { public static String extractFwVersion(@Nullable String version) { if (version != null) { - Matcher matcher = VERSION_PATTERN.matcher(version); + // fix version e.g. 20210319-122304/v.1.10-Dimmer1-gfd4cc10 (with v.1. instead of v1.) + String vers = version.replace("/v.1.10-", "/v1.10.0-"); + + // Extract version from string, e.g. 20210226-091047/v1.10.0-rc2-89-g623b41ec0-master + Matcher matcher = VERSION_PATTERN.matcher(vers); if (matcher.find()) { - // e.g. 20210226-091047/v1.10.0-rc2-89-g623b41ec0-master return matcher.group(0); } } 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 c57d9ef00a048..df7bd8a9437f9 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 @@ -32,6 +32,7 @@ import org.eclipse.jetty.http.HttpMethod; import org.eclipse.jetty.http.HttpStatus; import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellyControlRoller; +import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellyOtaCheckResult; import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySendKeyList; import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySenseKeyCode; import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsDevice; @@ -90,6 +91,14 @@ public ShellySettingsDevice getDevInfo() throws ShellyApiException { return callApi(SHELLY_URL_DEVINFO, ShellySettingsDevice.class); } + public String setDebug(boolean enabled) throws ShellyApiException { + return callApi(SHELLY_URL_SETTINGS + "?debug_enable=" + Boolean.valueOf(enabled), String.class); + } + + public String getDebugLog(String id) throws ShellyApiException { + return callApi("/debug/" + id, String.class); + } + /** * Initialize the device profile * @@ -241,6 +250,17 @@ public ShellySettingsLogin setLoginCredentials(String user, String password) thr ShellySettingsLogin.class); } + public String getCoIoTDescription() throws ShellyApiException { + try { + return callApi("/cit/d", String.class); + } catch (ShellyApiException e) { + if (e.getApiResult().isNotFound()) { + return ""; // only supported by FW 1.10+ + } + throw e; + } + } + public ShellySettingsLogin setCoIoTPeer(String peer) throws ShellyApiException { return callApi(SHELLY_URL_SETTINGS + "?coiot_enable=true&coiot_peer=" + peer, ShellySettingsLogin.class); } @@ -253,6 +273,23 @@ public String factoryReset() throws ShellyApiException { return callApi(SHELLY_URL_SETTINGS + "?reset=true", String.class); } + public ShellyOtaCheckResult checkForUpdate() throws ShellyApiException { + return callApi("/ota/check", ShellyOtaCheckResult.class); // nw FW 1.10+: trigger update check + } + + public String setWiFiRecovery(boolean enable) throws ShellyApiException { + return callApi(SHELLY_URL_SETTINGS + "?wifirecovery_reboot_enabled=" + (enable ? "true" : "false"), + String.class); // FW 1.10+: Enable auto-restart on WiFi problems + } + + public String setApRoaming(boolean enable) throws ShellyApiException { // FW 1.10+: Enable AP Roadming + return callApi(SHELLY_URL_SETTINGS + "?ap_roaming_enabled=" + (enable ? "true" : "false"), String.class); + } + + public String resetStaCache() throws ShellyApiException { // FW 1.10+: Reset cached STA/AP list and to a rescan + return callApi("/sta_cache_reset", String.class); + } + public ShellySettingsUpdate firmwareUpdate(String uri) throws ShellyApiException { return callApi("/ota?" + uri, ShellySettingsUpdate.class); } @@ -560,7 +597,8 @@ private ShellyApiResult innerRequest(HttpMethod method, String uri) throws Shell if (contentResponse.getStatus() != HttpStatus.OK_200) { throw new ShellyApiException(apiResult); } - if (response.isEmpty() || !response.startsWith("{") && !response.startsWith("[")) { + if (response.isEmpty() || !response.startsWith("{") && !response.startsWith("[") && !url.contains("/debug/") + && !url.contains("/sta_cache_reset")) { throw new ShellyApiException("Unexpected response: " + response); } } catch (ExecutionException | InterruptedException | TimeoutException | IllegalArgumentException e) { diff --git a/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/coap/ShellyCoIoTProtocol.java b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/coap/ShellyCoIoTProtocol.java index 7161f413d13bf..723f7607985bd 100644 --- a/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/coap/ShellyCoIoTProtocol.java +++ b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/coap/ShellyCoIoTProtocol.java @@ -22,6 +22,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.shelly.internal.api.ShellyDeviceProfile; +import org.openhab.binding.shelly.internal.api.ShellyHttpApi; import org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO.CoIotDescrBlk; import org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO.CoIotDescrSen; import org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO.CoIotSensor; @@ -50,6 +51,7 @@ public class ShellyCoIoTProtocol { protected final String thingName; protected final ShellyBaseHandler thingHandler; protected final ShellyDeviceProfile profile; + protected final ShellyHttpApi api; protected final Map blkMap; protected final Map sensorMap; private final Gson gson = new GsonBuilder().create(); @@ -68,6 +70,7 @@ public ShellyCoIoTProtocol(String thingName, ShellyBaseHandler thingHandler, Map this.blkMap = blkMap; this.sensorMap = sensorMap; this.profile = thingHandler.getProfile(); + this.api = thingHandler.getApi(); } protected boolean handleStatusUpdate(List sensorUpdates, CoIotDescrSen sen, CoIotSensor s, 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 378c5730d2e02..a35dc157fa1a9 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 @@ -37,6 +37,7 @@ import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.shelly.internal.api.ShellyApiException; import org.openhab.binding.shelly.internal.api.ShellyDeviceProfile; +import org.openhab.binding.shelly.internal.api.ShellyHttpApi; import org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO.CoIotDescrBlk; import org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO.CoIotDescrSen; import org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO.CoIotDevDescrTypeAdapter; @@ -80,7 +81,7 @@ public class ShellyCoapHandler implements ShellyCoapListener { private @Nullable CoapClient statusClient; private Request reqDescription = new Request(Code.GET, Type.CON); private Request reqStatus = new Request(Code.GET, Type.CON); - private boolean discovering = false; + private boolean updatesRequested = false; private int coiotPort = COIOT_PORT; private long coiotMessages = 0; @@ -90,11 +91,13 @@ public class ShellyCoapHandler implements ShellyCoapListener { private Map blkMap = new LinkedHashMap<>(); private Map sensorMap = new LinkedHashMap<>(); private ShellyDeviceProfile profile; + private ShellyHttpApi api; public ShellyCoapHandler(ShellyBaseHandler thingHandler, ShellyCoapServer coapServer) { this.thingHandler = thingHandler; this.thingName = thingHandler.thingName; this.profile = thingHandler.getProfile(); + this.api = thingHandler.getApi(); this.coapServer = coapServer; this.coiot = new ShellyCoIoTVersion2(thingName, thingHandler, blkMap, sensorMap); // Default: V2 @@ -137,6 +140,7 @@ public synchronized void start(String thingName, ShellyThingConfiguration config logger.warn("{}: Unable to initialize CoAP access (network error)", thingName); throw new ShellyApiException("Network initialization failed"); } + discover(); } catch (SocketException e) { logger.warn("{}: Unable to initialize CoAP access (socket exception) - {}", thingName, e.getMessage()); @@ -283,10 +287,10 @@ public void processResponse(@Nullable Response response) { coiotErrors++; } - if (!discovering) { + if (!updatesRequested) { // Observe Status Updates reqStatus = sendRequest(reqStatus, config.deviceIp, COLOIT_URI_DEVSTATUS, Type.NON); - discovering = true; + updatesRequested = true; } } catch (JsonSyntaxException | IllegalArgumentException | NullPointerException e) { logger.debug("{}: Unable to process CoIoT Message for payload={}", thingName, payload, e); @@ -538,6 +542,21 @@ i, s.id, getString(s.valueStr).isEmpty() ? s.value : s.valueStr, sen.desc, sen.t } private void discover() { + if (coiot.getVersion() >= 2) { + { + try { + // Try to device description using http request (FW 1.10+) + String payload = api.getCoIoTDescription(); + if (!payload.isEmpty()) { + logger.debug("{}: Using CoAP device description from successful HTTP /cit/d", thingName); + handleDeviceDescription(thingName, payload); + return; + } + } catch (ShellyApiException e) { + // ignore if not supported by device + } + } + } reqDescription = sendRequest(reqDescription, config.deviceIp, COLOIT_URI_DEVDESC, Type.CON); } diff --git a/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/handler/ShellyBaseHandler.java b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/handler/ShellyBaseHandler.java index 064621e57b51c..25d6b43022f15 100755 --- a/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/handler/ShellyBaseHandler.java +++ b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/handler/ShellyBaseHandler.java @@ -32,6 +32,7 @@ import org.openhab.binding.shelly.internal.api.ShellyApiException; import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO; import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellyInputState; +import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellyOtaCheckResult; import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsDevice; import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsStatus; import org.openhab.binding.shelly.internal.api.ShellyApiResult; @@ -245,9 +246,8 @@ private boolean initializeThing() throws ShellyApiException { } tmpPrf.auth = devInfo.auth; // missing in /settings - logger.debug("{}: Initializing device {}, type {}, Hardware: Rev: {}, batch {}; Firmware: {} / {} ({})", - thingName, tmpPrf.hostname, tmpPrf.deviceType, tmpPrf.hwRev, tmpPrf.hwBatchId, tmpPrf.fwVersion, - tmpPrf.fwDate, tmpPrf.fwId); + logger.debug("{}: Initializing device {}, type {}, Hardware: Rev: {}, batch {}; Firmware: {} / {}", thingName, + tmpPrf.hostname, tmpPrf.deviceType, tmpPrf.hwRev, tmpPrf.hwBatchId, tmpPrf.fwVersion, tmpPrf.fwDate); logger.debug("{}: Shelly settings info for {}: {}", thingName, tmpPrf.hostname, tmpPrf.settingsJson); logger.debug("{}: Device " + "hasRelays:{} (numRelays={}),isRoller:{} (numRoller={}),isDimmer:{},numMeter={},isEMeter:{})" @@ -273,7 +273,7 @@ private boolean initializeThing() throws ShellyApiException { } catch (ShellyApiException e) { logger.debug("{}: Unable to set CoIoT peer: {}", thingName, e.toString()); } - } else if (!devpeer.equals(ourpeer)) { + } else if (!devpeer.isEmpty() && !devpeer.equals(ourpeer)) { logger.warn("{}: CoIoT peer in device settings does not point this to this host, disabling CoIoT", thingName); config.eventsCoIoT = autoCoIoT = false; @@ -396,9 +396,15 @@ protected void refreshStatus() { // Get profile, if refreshSettings == true reload settings from device logger.trace("{}: Updating status (refreshSettings={})", thingName, refreshSettings); ShellySettingsStatus status = api.getStatus(); - profile = getProfile(refreshSettings || checkRestarted(status)); + boolean restarted = checkRestarted(status); + profile = getProfile(refreshSettings || restarted); profile.status = status; profile.updateFromStatus(status); + if (restarted) { + logger.debug("{}: Device restart #{} detected", thingName, stats.restarts); + stats.restarts++; + postEvent(ALARM_TYPE_RESTARTED, true); + } // If status update was successful the thing must be online setThingOnline(); @@ -571,9 +577,6 @@ private void fillDeviceStatus(ShellySettingsStatus status, boolean updated) { private boolean checkRestarted(ShellySettingsStatus status) { if (profile.isInitialized() && (status.uptime < stats.lastUptime || !profile.status.update.oldVersion.isEmpty() && !status.update.oldVersion.equals(profile.status.update.oldVersion))) { - logger.debug("{}: Device restart #{} detected", thingName, stats.restarts); - stats.restarts++; - postEvent(ALARM_TYPE_RESTARTED, true); updateProperties(profile, status); return true; } @@ -799,12 +802,11 @@ private void checkVersion(ShellyDeviceProfile prf, ShellySettingsStatus status) try { ShellyVersionDTO version = new ShellyVersionDTO(); if (version.checkBeta(getString(prf.fwVersion))) { - logger.info("{}: {}", prf.hostname, messages.get("versioncheck.beta", prf.fwVersion, prf.fwDate, - prf.fwId, SHELLY_API_MIN_FWVERSION)); + logger.info("{}: {}", prf.hostname, messages.get("versioncheck.beta", prf.fwVersion, prf.fwDate)); } else { if ((version.compare(prf.fwVersion, SHELLY_API_MIN_FWVERSION) < 0) && !profile.isMotion) { - logger.warn("{}: {}", prf.hostname, messages.get("versioncheck.tooold", prf.fwVersion, prf.fwDate, - prf.fwId, SHELLY_API_MIN_FWVERSION)); + logger.warn("{}: {}", prf.hostname, + messages.get("versioncheck.tooold", prf.fwVersion, prf.fwDate, SHELLY_API_MIN_FWVERSION)); } } if (bindingConfig.autoCoIoT && ((version.compare(prf.fwVersion, SHELLY_API_MIN_FWCOIOT)) >= 0) @@ -1081,7 +1083,6 @@ protected void updateProperties(ShellyDeviceProfile profile, ShellySettingsStatu properties.put(PROPERTY_UPDATE_NEW_VERS, getString(status.update.newVersion)); } properties.put(PROPERTY_COIOTAUTO, String.valueOf(autoCoIoT)); - properties.put(PROPERTY_COIOTREFRESH, String.valueOf(autoCoIoT)); Map thingProperties = new TreeMap<>(); for (Map.Entry property : properties.entrySet()) { @@ -1142,8 +1143,7 @@ public static Map fillDeviceProperties(ShellyDeviceProfile profi if (profile.isInitialized()) { properties.put(PROPERTY_MODEL_ID, getString(profile.settings.device.type)); properties.put(PROPERTY_MAC_ADDRESS, profile.mac); - properties.put(PROPERTY_FIRMWARE_VERSION, - profile.fwVersion + "/" + profile.fwDate + "(" + profile.fwId + ")"); + properties.put(PROPERTY_FIRMWARE_VERSION, profile.fwVersion + "/" + profile.fwDate); properties.put(PROPERTY_DEV_MODE, profile.mode); properties.put(PROPERTY_NUM_RELAYS, String.valueOf(profile.numRelays)); properties.put(PROPERTY_NUM_ROLLERS, String.valueOf(profile.numRollers)); @@ -1264,4 +1264,13 @@ public ShellyHttpApi getApi() { public Map getStatsProp() { return stats.asProperties(); } + + public String checkForUpdate() { + try { + ShellyOtaCheckResult result = api.checkForUpdate(); + return result.status; + } catch (ShellyApiException e) { + return ""; + } + } } diff --git a/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/manager/ShellyManager.java b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/manager/ShellyManager.java new file mode 100644 index 0000000000000..388b2a36b71bf --- /dev/null +++ b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/manager/ShellyManager.java @@ -0,0 +1,65 @@ +/** + * Copyright (c) 2010-2021 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.manager; + +import static org.openhab.binding.shelly.internal.manager.ShellyManagerConstants.*; + +import java.util.LinkedHashMap; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.http.HttpStatus; +import org.openhab.binding.shelly.internal.ShellyHandlerFactory; +import org.openhab.binding.shelly.internal.api.ShellyApiException; +import org.openhab.binding.shelly.internal.manager.ShellyManagerPage.ShellyMgrResponse; +import org.openhab.binding.shelly.internal.provider.ShellyTranslationProvider; +import org.osgi.service.cm.ConfigurationAdmin; + +/** + * {@link ShellyManager} implements the Shelly Manager + * + * @author Markus Michels - Initial contribution + */ +@NonNullByDefault +public class ShellyManager { + private final Map pages = new LinkedHashMap<>(); + private final ShellyHandlerFactory handlerFactory; + + public ShellyManager(ConfigurationAdmin configurationAdmin, ShellyTranslationProvider translationProvider, + HttpClient httpClient, String localIp, int localPort, ShellyHandlerFactory handlerFactory) { + this.handlerFactory = handlerFactory; + pages.put(SHELLY_MGR_OVERVIEW_URI, new ShellyManagerOverviewPage(configurationAdmin, translationProvider, + httpClient, localIp, localPort, handlerFactory)); + pages.put(SHELLY_MGR_ACTION_URI, new ShellyManagerActionPage(configurationAdmin, translationProvider, + httpClient, localIp, localPort, handlerFactory)); + pages.put(SHELLY_MGR_FWUPDATE_URI, new ShellyManagerOtaPage(configurationAdmin, translationProvider, httpClient, + localIp, localPort, handlerFactory)); + pages.put(SHELLY_MGR_OTA_URI, new ShellyManagerOtaPage(configurationAdmin, translationProvider, httpClient, + localIp, localPort, handlerFactory)); + pages.put(SHELLY_MGR_IMAGES_URI, new ShellyManagerImageLoader(configurationAdmin, translationProvider, + httpClient, localIp, localPort, handlerFactory)); + pages.put(SHELLY_MANAGER_URI, new ShellyManagerOverviewPage(configurationAdmin, translationProvider, httpClient, + localIp, localPort, handlerFactory)); + } + + public ShellyMgrResponse generateContent(String path, Map parameters) throws ShellyApiException { + for (Map.Entry page : pages.entrySet()) { + if (path.toLowerCase().startsWith(page.getKey())) { + ShellyManagerPage p = page.getValue(); + return p.generateContent(path, parameters); + } + } + return new ShellyMgrResponse("Invalid URL or syntax", HttpStatus.BAD_REQUEST_400); + } +} diff --git a/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/manager/ShellyManagerActionPage.java b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/manager/ShellyManagerActionPage.java new file mode 100644 index 0000000000000..927fbe6cbe2fe --- /dev/null +++ b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/manager/ShellyManagerActionPage.java @@ -0,0 +1,356 @@ +/** + * Copyright (c) 2010-2021 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.manager; + +import static org.openhab.binding.shelly.internal.ShellyBindingConstants.PROPERTY_SERVICE_NAME; +import static org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.SHELLY_COIOT_MCAST; +import static org.openhab.binding.shelly.internal.manager.ShellyManagerConstants.*; +import static org.openhab.binding.shelly.internal.util.ShellyUtils.*; + +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.http.HttpStatus; +import org.openhab.binding.shelly.internal.ShellyHandlerFactory; +import org.openhab.binding.shelly.internal.api.ShellyApiException; +import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellyOtaCheckResult; +import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsLogin; +import org.openhab.binding.shelly.internal.api.ShellyDeviceProfile; +import org.openhab.binding.shelly.internal.api.ShellyHttpApi; +import org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO; +import org.openhab.binding.shelly.internal.config.ShellyThingConfiguration; +import org.openhab.binding.shelly.internal.handler.ShellyManagerInterface; +import org.openhab.binding.shelly.internal.provider.ShellyTranslationProvider; +import org.openhab.core.thing.ThingStatusDetail; +import org.osgi.service.cm.ConfigurationAdmin; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * {@link ShellyManagerActionPage} implements the Shelly Manager's action page + * + * @author Markus Michels - Initial contribution + */ +@NonNullByDefault +public class ShellyManagerActionPage extends ShellyManagerPage { + private final Logger logger = LoggerFactory.getLogger(ShellyManagerActionPage.class); + + public ShellyManagerActionPage(ConfigurationAdmin configurationAdmin, ShellyTranslationProvider translationProvider, + HttpClient httpClient, String localIp, int localPort, ShellyHandlerFactory handlerFactory) { + super(configurationAdmin, translationProvider, httpClient, localIp, localPort, handlerFactory); + } + + @Override + public ShellyMgrResponse generateContent(String path, Map parameters) throws ShellyApiException { + String action = getUrlParm(parameters, URLPARM_ACTION); + String uid = getUrlParm(parameters, URLPARM_UID); + String update = getUrlParm(parameters, URLPARM_UPDATE); + if (uid.isEmpty() || action.isEmpty()) { + return new ShellyMgrResponse("Invalid URL parameters: " + parameters.toString(), + HttpStatus.BAD_REQUEST_400); + } + + Map properties = new HashMap<>(); + properties.put(ATTRIBUTE_METATAG, ""); + properties.put(ATTRIBUTE_CSS_HEADER, ""); + properties.put(ATTRIBUTE_CSS_FOOTER, ""); + String html = loadHTML(HEADER_HTML, properties); + + ShellyManagerInterface th = getThingHandler(uid); + if (th != null) { + fillProperties(properties, uid, th); + + Map actions = getActions(th.getProfile()); + String actionUrl = SHELLY_MGR_OVERVIEW_URI; + String actionButtonLabel = "OK"; // Default + String serviceName = getValue(properties, PROPERTY_SERVICE_NAME); + String message = ""; + + ShellyThingConfiguration config = getThingConfig(th, properties); + ShellyDeviceProfile profile = th.getProfile(); + ShellyHttpApi api = th.getApi(); + new ShellyHttpApi(uid, config, httpClient); + + int refreshTimer = 0; + switch (action) { + case ACTION_RES_STATS: + th.resetStats(); + message = getMessageP("action.resstats.confirm", MCINFO); + refreshTimer = 3; + break; + case ACTION_RESTART: + if (update.equalsIgnoreCase("yes")) { + message = getMessageP("action.restart.info", MCINFO); + actionButtonLabel = "Ok"; + new Thread(() -> { // schedule asynchronous reboot + try { + api.deviceReboot(); + } catch (ShellyApiException e) { + // maybe the device restarts before returning the http response + } + setRestarted(th, uid); // refresh after reboot + }).start(); + refreshTimer = profile.isMotion ? 60 : 30; + } else { + message = getMessageS("action.restart.confirm", MCINFO); + actionUrl = buildActionUrl(uid, action); + } + break; + case ACTION_PROTECT: + // Get device settings + if (config.userId.isEmpty() || config.password.isEmpty()) { + message = getMessageP("action.protect.id-missing", MCWARNING); + break; + } + + if (!update.equalsIgnoreCase("yes")) { + ShellySettingsLogin status = api.getLoginSettings(); + message = getMessage("action.protect.status", getBool(status.enabled) ? "enabled" : "disabled", + status.username) + + getMessageP("action.protect.new", MCINFO, config.userId, config.password); + actionUrl = buildActionUrl(uid, action); + } else { + api.setLoginCredentials(config.userId, config.password); + message = getMessageP("action.protect.confirm", MCINFO, config.userId, config.password); + refreshTimer = 3; + } + break; + case ACTION_SETCOIOT_MCAST: + case ACTION_SETCOIOT_PEER: + if ((profile.settings.coiot == null) || (profile.settings.coiot.peer == null)) { + // feature not available + message = getMessage("coiot.mode-not-suppored", MCWARNING, action); + break; + } + + String peer = getString(profile.settings.coiot.peer); + boolean mcast = peer.isEmpty() || peer.equalsIgnoreCase(SHELLY_COIOT_MCAST); + String newPeer = mcast ? localIp + ":" + ShellyCoapJSonDTO.COIOT_PORT : SHELLY_COIOT_MCAST; + String displayPeer = mcast ? newPeer : "Multicast"; + + if (profile.isMotion && action.equalsIgnoreCase(ACTION_SETCOIOT_MCAST)) { + // feature not available + message = getMessageP("coiot.multicast-not-supported", "warning", displayPeer); + break; + } + + if (!update.equalsIgnoreCase("yes")) { + message = getMessageP("coiot.current-peer", MCMESSAGE, mcast ? "Multicast" : peer) + + getMessageP("coiot.new-peer", MCINFO, displayPeer) + + getMessageP(mcast ? "coiot.mode-peer" : "coiot.mode-mcast", MCMESSAGE); + actionUrl = buildActionUrl(uid, action); + } else { + new Thread(() -> { // schedule asynchronous reboot + try { + api.setCoIoTPeer(newPeer); + api.deviceReboot(); + } catch (ShellyApiException e) { + // maybe the device restarts before returning the http response + } + setRestarted(th, uid); // refresh after reboot + }).start(); + + // The device needs a restart after changing the peer mode + message = getMessageP("action.restart.info", MCINFO); + refreshTimer = 30; + } + break; + case ACTION_ENCLOUD: + case ACTION_DISCLOUD: + boolean enabled = action.equals(ACTION_ENCLOUD); + api.setCloud(enabled); + message = getMessageP("action.setcloud.config", MCINFO, enabled ? "enabled" : "disabled"); + refreshTimer = 20; + break; + case ACTION_RESET: + if (!update.equalsIgnoreCase("yes")) { + message = getMessageP("action.reset.warning", MCWARNING, serviceName); + actionUrl = buildActionUrl(uid, action); + } else { + new Thread(() -> { // schedule asynchronous reboot + try { + api.factoryReset(); + setRestarted(th, uid); + } catch (ShellyApiException e) { + // maybe the device restarts before returning the http response + } + }).start(); + message = getMessageP("action.reset.confirm", MCINFO, serviceName); + refreshTimer = 5; + } + break; + case ACTION_OTACHECK: + try { + ShellyOtaCheckResult result = api.checkForUpdate(); + message = getMessage("action.checkupd." + result.status); + } catch (ShellyApiException e) { + // maybe the device restarts before returning the http response + message = getMessageP("action.checkupd.failed", e.toString()); + } + refreshTimer = 3; + break; + case ACTION_ENDEBUG: + case ACTION_DISDEBUG: + boolean enable = action.equalsIgnoreCase(ACTION_ENDEBUG); + if (!update.equalsIgnoreCase("yes")) { + message = getMessage(enable ? "action.debug-enable" : "action.debug-disable"); + actionUrl = buildActionUrl(uid, action); + } else { + new Thread(() -> { // schedule asynchronous reboot + try { + api.setDebug(enable); + } catch (ShellyApiException e) { + // maybe the device restarts before returning the http response + } + }).start(); + + message = getMessage("action.debug-confirm", enable ? "enabled" : "disabled"); + refreshTimer = 3; + } + break; + case ACTION_RESSTA: + if (!update.equalsIgnoreCase("yes")) { + message = getMessage("action.resetsta-info"); + actionUrl = buildActionUrl(uid, action); + } else { + try { + String result = api.resetStaCache(); + message = getMessage("action.resetsta-confirm"); + } catch (ShellyApiException e) { + message = getMessageP("action.resetsta-failed", e.toString()); + } + refreshTimer = 10; + } + break; + case ACTION_ENWIFIREC: + case ACTION_DISWIFIREC: + enable = action.equalsIgnoreCase(ACTION_ENWIFIREC); + if (!update.equalsIgnoreCase("yes")) { + message = getMessage(enable ? "action.setwifirec-enable" : "action.setwifirec-disable"); + actionUrl = buildActionUrl(uid, action); + } else { + try { + String result = api.setWiFiRecovery(enable); + message = getMessage("action.setwifirec-confirm", enable ? "enabled" : "disabled"); + } catch (ShellyApiException e) { + message = getMessage("action.setwifirec-failed", e.toString()); + } + refreshTimer = 3; + } + break; + + case ACTION_ENAPROAMING: + case ACTION_DISAPROAMING: + enable = action.equalsIgnoreCase(ACTION_ENAPROAMING); + if (!update.equalsIgnoreCase("yes")) { + message = getMessage(enable ? "action.aproaming-enable" : "action.aproaming-disable"); + actionUrl = buildActionUrl(uid, action); + } else { + try { + String result = api.setApRoaming(enable); + message = getMessage("action.aproaming-confirm", enable ? "enabled" : "disabled"); + } catch (ShellyApiException e) { + message = getMessage("action.aproaming-failed", e.toString()); + } + refreshTimer = 3; + } + break; + + case ACTION_GETDEB: + case ACTION_GETDEB1: + try { + message = api.getDebugLog(action.equalsIgnoreCase(ACTION_GETDEB) ? "log" : "log1"); + message = message.replaceAll("[\r]", "").replaceAll("[\r\n]", "
"); + } catch (ShellyApiException e) { + message = getMessage("action.getdebug-failed", e.toString()); + } + break; + case ACTION_NONE: + break; + default: + logger.warn("{}: {}", LOG_PREFIX, getMessage("action.unknown", action)); + } + + properties.put(ATTRIBUTE_ACTION, getString(actions.get(action))); // get description for command + properties.put(ATTRIBUTE_ACTION_BUTTON, actionButtonLabel); + properties.put(ATTRIBUTE_ACTION_URL, actionUrl); + message = fillAttributes(message, properties); + properties.put(ATTRIBUTE_MESSAGE, message); + properties.put(ATTRIBUTE_REFRESH, String.valueOf(refreshTimer)); + html += loadHTML(ACTION_HTML, properties); + + th.requestUpdates(1, refreshTimer > 0); // trigger background update + } + + properties.clear(); + html += loadHTML(FOOTER_HTML, properties); + return new ShellyMgrResponse(html, HttpStatus.OK_200); + } + + public static Map getActions(ShellyDeviceProfile profile) { + Map list = new LinkedHashMap<>(); + list.put(ACTION_RES_STATS, "Reset Statistics"); + list.put(ACTION_RESTART, "Reboot Device"); + list.put(ACTION_PROTECT, "Protect Device"); + + if ((profile.settings.coiot != null) && (profile.settings.coiot.peer != null) && !profile.isMotion) { + boolean mcast = profile.settings.coiot.peer.isEmpty() + || profile.settings.coiot.peer.equalsIgnoreCase(SHELLY_COIOT_MCAST); + list.put(mcast ? ACTION_SETCOIOT_PEER : ACTION_SETCOIOT_MCAST, + mcast ? "Set CoIoT Peer Mode" : "Set CoIoT Multicast Mode"); + } + if (profile.isSensor && !profile.isMotion && (profile.settings.wifiSta != null) + && profile.settings.wifiSta.enabled) { + // FW 1.10+: Reset STA list, force WiFi rescan and connect to stringest AP + list.put(ACTION_RESSTA, "Reconnect WiFi"); + } + if (profile.settings.apRoaming != null) { + list.put(!profile.settings.apRoaming.enabled ? ACTION_ENAPROAMING : ACTION_DISAPROAMING, + !profile.settings.apRoaming.enabled ? "Enable WiFi Roaming" : "Disable WiFi Roaming"); + } + if (profile.settings.wifiRecoveryReboot != null) { + list.put(!profile.settings.wifiRecoveryReboot ? ACTION_ENWIFIREC : ACTION_DISWIFIREC, + !profile.settings.wifiRecoveryReboot ? "Enable WiFi Recovery" : "Disable WiFi Recovery"); + } + + boolean set = (profile.settings.cloud != null) && profile.settings.cloud.enabled; + list.put(set ? ACTION_DISCLOUD : ACTION_ENCLOUD, set ? "Disable Cloud" : "Enable Cloud"); + + list.put(ACTION_RESET, "-Factory Reset"); + if (profile.extFeatures) { + list.put(ACTION_OTACHECK, "Check for Update"); + boolean debug_enable = getBool(profile.settings.debug_enable); + list.put(!debug_enable ? ACTION_ENDEBUG : ACTION_DISDEBUG, + !debug_enable ? "Enable Debug" : "Disable Debug"); + if (debug_enable) { + list.put(ACTION_GETDEB, "Get Debug log"); + list.put(ACTION_GETDEB1, "Get Debug log1"); + } + } + + return list; + } + + private String buildActionUrl(String uid, String action) { + return SHELLY_MGR_ACTION_URI + "?" + URLPARM_ACTION + "=" + action + "&" + URLPARM_UID + "=" + urlEncode(uid) + + "&" + URLPARM_UPDATE + "=yes"; + } + + private void setRestarted(ShellyManagerInterface th, String uid) { + th.setThingOffline(ThingStatusDetail.GONE, "offline.status-error-restarted"); + scheduleUpdate(th, uid + "_upgrade", 25); // wait 25s before refresh + } +} diff --git a/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/manager/ShellyManagerCache.java b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/manager/ShellyManagerCache.java new file mode 100644 index 0000000000000..5af3e0fb8e513 --- /dev/null +++ b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/manager/ShellyManagerCache.java @@ -0,0 +1,100 @@ +/** + * Copyright (c) 2010-2021 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.manager; + +import java.util.Date; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * {@link ShellyManagerCache} implements a cache with expiring times of the entries + * + * @author Markus Michels - Initial contribution + */ +@NonNullByDefault +public class ShellyManagerCache extends ConcurrentHashMap { + + private static final long serialVersionUID = 1L; + + private Map timeMap = new ConcurrentHashMap(); + private long expiryInMillis = ShellyManagerConstants.CACHE_TIMEOUT_DEF_MIN * 60 * 1000; // Default 1h + + public ShellyManagerCache() { + initialize(); + } + + public ShellyManagerCache(long expiryInMillis) { + this.expiryInMillis = expiryInMillis; + initialize(); + } + + void initialize() { + new CleanerThread().start(); + } + + @Override + public @Nullable V put(K key, V value) { + Date date = new Date(); + timeMap.put(key, date.getTime()); + V returnVal = super.put(key, value); + return returnVal; + } + + @Override + public void putAll(@Nullable Map m) { + if (m == null) { + throw new IllegalArgumentException(); + } + for (K key : m.keySet()) { + V value = m.get(key); + if (value != null) { // don't allow null values + put(key, value); + } + } + } + + @Override + public @Nullable V putIfAbsent(K key, V value) { + if (!containsKey(key)) { + return put(key, value); + } else { + return get(key); + } + } + + class CleanerThread extends Thread { + @Override + public void run() { + while (true) { + cleanMap(); + try { + Thread.sleep(expiryInMillis / 2); + } catch (InterruptedException e) { + } + } + } + + private void cleanMap() { + long currentTime = new Date().getTime(); + for (K key : timeMap.keySet()) { + if (currentTime > (timeMap.get(key) + expiryInMillis)) { + V value = remove(key); + timeMap.remove(key); + } + } + } + } +} diff --git a/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/manager/ShellyManagerConstants.java b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/manager/ShellyManagerConstants.java new file mode 100644 index 0000000000000..5f6151ac072c1 --- /dev/null +++ b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/manager/ShellyManagerConstants.java @@ -0,0 +1,149 @@ +/** + * Copyright (c) 2010-2021 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.manager; + +import java.nio.charset.StandardCharsets; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * {@link ShellyManagerConstants} defines the constants for Shelly Manager + * + * @author Markus Michels - Initial contribution + */ +@NonNullByDefault +public class ShellyManagerConstants { + public static final String LOG_PREFIX = "ShellyManager"; + public static final String UTF_8 = StandardCharsets.UTF_8.toString(); + + public static final String SHELLY_MANAGER_URI = "/shelly/manager"; + public static final String SHELLY_MGR_OVERVIEW_URI = SHELLY_MANAGER_URI + "/ovierview"; + public static final String SHELLY_MGR_FWUPDATE_URI = SHELLY_MANAGER_URI + "/fwupdate"; + public static final String SHELLY_MGR_IMAGES_URI = SHELLY_MANAGER_URI + "/images"; + public static final String SHELLY_MGR_ACTION_URI = SHELLY_MANAGER_URI + "/action"; + public static final String SHELLY_MGR_OTA_URI = SHELLY_MANAGER_URI + "/ota"; + + public static final String ACTION_REFRESH = "refresh"; + public static final String ACTION_RESTART = "restart"; + public static final String ACTION_PROTECT = "protect"; + public static final String ACTION_SETCOIOT_PEER = "setcoiotpeer"; + public static final String ACTION_SETCOIOT_MCAST = "setcoiotmcast"; + public static final String ACTION_SETTZ = "settz"; + public static final String ACTION_SETNTP = "setntp"; + public static final String ACTION_ENCLOUD = "encloud"; + public static final String ACTION_DISCLOUD = "discloud"; + public static final String ACTION_RES_STATS = "reset_stat"; + public static final String ACTION_RESET = "reset"; + public static final String ACTION_RESSTA = "resetsta"; + public static final String ACTION_ENWIFIREC = "enwifirec"; + public static final String ACTION_DISWIFIREC = "diswifirec"; + public static final String ACTION_ENAPROAMING = "enaproaming"; + public static final String ACTION_DISAPROAMING = "disaproaming"; + public static final String ACTION_OTACHECK = "otacheck"; + public static final String ACTION_ENDEBUG = "endebug"; + public static final String ACTION_DISDEBUG = "disdebug"; + public static final String ACTION_GETDEB = "getdebug"; + public static final String ACTION_GETDEB1 = "getdebug1"; + public static final String ACTION_NONE = "-"; + + public static final String TEMPLATE_PATH = "sniplets/"; + public static final String HEADER_HTML = "header.html"; + public static final String OVERVIEW_HTML = "overview.html"; + public static final String OVERVIEW_HEADER = "ov_header.html"; + public static final String OVERVIEW_DEVICE = "ov_device.html"; + public static final String OVERVIEW_FOOTER = "ov_footer.html"; + public static final String FWUPDATE1_HTML = "fw_update1.html"; + public static final String FWUPDATE2_HTML = "fw_update2.html"; + public static final String ACTION_HTML = "action.html"; + public static final String FOOTER_HTML = "footer.html"; + public static final String IMAGE_PATH = "images/"; + public static final String FORWARD_SCRIPT = "forward.script"; + + public static final String ATTRIBUTE_METATAG = "metaTag"; + public static final String ATTRIBUTE_CSS_HEADER = "cssHeader"; + public static final String ATTRIBUTE_CSS_FOOTER = "cssFooter"; + public static final String ATTRIBUTE_URI = "uri"; + public static final String ATTRIBUTE_UID = "uid"; + public static final String ATTRIBUTE_REFRESH = "refreshTimer"; + public static final String ATTRIBUTE_MESSAGE = "message"; + public static final String ATTRIBUTE_TOTAL_DEV = "totalDevices"; + public static final String ATTRIBUTE_STATUS_ICON = "iconStatus"; + public static final String ATTRIBUTE_DISPLAY_NAME = "displayName"; + public static final String ATTRIBUTE_DEV_STATUS = "deviceStatus"; + public static final String ATTRIBUTE_DEBUG_MODE = "debugMode"; + public static final String ATTRIBUTE_FIRMWARE_SEL = "firmwareSelection"; + public static final String ATTRIBUTE_ACTION_LIST = "actionList"; + public static final String ATTRIBUTE_VERSION = "version"; + public static final String ATTRIBUTE_FW_URL = "firmwareUrl"; + public static final String ATTRIBUTE_UPDATE_URL = "updateUrl"; + public static final String ATTRIBUTE_LAST_ALARM = "lastAlarmTs"; + public static final String ATTRIBUTE_ACTION = "action"; + public static final String ATTRIBUTE_ACTION_BUTTON = "actionButtonLabel"; + public static final String ATTRIBUTE_ACTION_URL = "actionUrl"; + public static final String ATTRIBUTE_SNTP_SERVER = "sntpServer"; + public static final String ATTRIBUTE_COIOT_STATUS = "coiotStatus"; + public static final String ATTRIBUTE_COIOT_PEER = "coiotDestination"; + public static final String ATTRIBUTE_CLOUD_STATUS = "cloudStatus"; + public static final String ATTRIBUTE_MQTT_STATUS = "mqttStatus"; + public static final String ATTRIBUTE_ACTIONS_SKIPPED = "actionsSkipped"; + public static final String ATTRIBUTE_DISCOVERABLE = "discoverable"; + public static final String ATTRIBUTE_WIFI_RECOVERY = "wifiAutoRecovery"; + public static final String ATTRIBUTE_APR_MODE = "apRoamingMode"; + public static final String ATTRIBUTE_APR_TRESHOLD = "apRoamingThreshold"; + public static final String ATTRIBUTE_MAX_ITEMP = "maxInternalTemp"; + public static final String ATTRIBUTE_TIMEZONE = "deviceTimezone"; + public static final String ATTRIBUTE_PWD_PROTECT = "passwordProtected"; + + public static final String URLPARM_UID = "uid"; + public static final String URLPARM_DEVTYPE = "deviceType"; + public static final String URLPARM_DEVMODE = "deviceMode"; + public static final String URLPARM_ACTION = "action"; + public static final String URLPARM_FILTER = "filter"; + public static final String URLPARM_TYPE = "type"; + public static final String URLPARM_VERSION = "version"; + public static final String URLPARM_UPDATE = "update"; + public static final String URLPARM_CONNECTION = "connection"; + public static final String URLPARM_URL = "url"; + + public static final String FILTER_ONLINE = "online"; + public static final String FILTER_INACTIVE = "inactive"; + public static final String FILTER_ATTENTION = "attention"; + public static final String FILTER_UPDATE = "update"; + public static final String FILTER_UNPROTECTED = "unprotected"; + + // Message classes for visual style + public static final String MCMESSAGE = "message"; + public static final String MCINFO = "info"; + public static final String MCWARNING = "warning"; + + public static final String ICON_ONLINE = "online"; + public static final String ICON_OFFLINE = "offline"; + public static final String ICON_UNINITIALIZED = "uninitialized"; + public static final String ICON_CONFIG = "config"; + public static final String ICON_ATTENTION = "attention"; + + public static final String CONNECTION_TYPE_LOCAL = "local"; + public static final String CONNECTION_TYPE_INTERNET = "internet"; + public static final String CONNECTION_TYPE_CUSTOM = "custom"; + + public static final String FWPROD = "prod"; + public static final String FWBETA = "beta"; + + public static final String FWREPO_PROD_URL = "https://api.shelly.cloud/files/firmware/"; + public static final String FWREPO_TEST_URL = "https://repo.shelly.cloud/files/firmware/"; + public static final String FWREPO_ARCH_URL = "http://archive.shelly-tools.de/archive.php"; + public static final String FWREPO_ARCFILE_URL = "http://archive.shelly-tools.de/version/"; + + public static final int CACHE_TIMEOUT_DEF_MIN = 60; // Default timeout for cache entries + public static final int CACHE_TIMEOUT_FW_MIN = 15; // Cache entries for the firmware list 15min +} diff --git a/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/manager/ShellyManagerImageLoader.java b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/manager/ShellyManagerImageLoader.java new file mode 100644 index 0000000000000..dfdad7ec706a8 --- /dev/null +++ b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/manager/ShellyManagerImageLoader.java @@ -0,0 +1,70 @@ +/** + * Copyright (c) 2010-2021 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.manager; + +import static org.openhab.binding.shelly.internal.manager.ShellyManagerConstants.IMAGE_PATH; +import static org.openhab.binding.shelly.internal.util.ShellyUtils.substringAfter; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.http.HttpStatus; +import org.openhab.binding.shelly.internal.ShellyHandlerFactory; +import org.openhab.binding.shelly.internal.api.ShellyApiException; +import org.openhab.binding.shelly.internal.handler.ShellyManagerInterface; +import org.openhab.binding.shelly.internal.provider.ShellyTranslationProvider; +import org.osgi.service.cm.ConfigurationAdmin; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * {@link ShellyManagerImageLoader} implements the Shelly Manager's download proxy for images (load them from bundle) + * + * @author Markus Michels - Initial contribution + */ +@NonNullByDefault +public class ShellyManagerImageLoader extends ShellyManagerPage { + private final Logger logger = LoggerFactory.getLogger(ShellyManagerImageLoader.class); + + public ShellyManagerImageLoader(ConfigurationAdmin configurationAdmin, + ShellyTranslationProvider translationProvider, HttpClient httpClient, String localIp, int localPort, + ShellyHandlerFactory handlerFactory) { + super(configurationAdmin, translationProvider, httpClient, localIp, localPort, handlerFactory); + } + + @Override + public ShellyMgrResponse generateContent(String path, Map parameters) throws ShellyApiException { + return loadImage(substringAfter(path, ShellyManagerConstants.SHELLY_MGR_IMAGES_URI + "/")); + } + + protected ShellyMgrResponse loadImage(String image) throws ShellyApiException { + String file = IMAGE_PATH + image; + logger.trace("Read Image from {}", file); + ClassLoader cl = ShellyManagerInterface.class.getClassLoader(); + if (cl != null) { + try (InputStream inputStream = cl.getResourceAsStream(file)) { + if (inputStream != null) { + byte[] buf = new byte[inputStream.available()]; + inputStream.read(buf); + return new ShellyMgrResponse(buf, HttpStatus.OK_200, "image/png"); + } + } catch (IOException | RuntimeException e) { + logger.debug("ShellyManager: Unable to read {} from bundle resources!", image, e); + } + } + return new ShellyMgrResponse("Unable to read " + image + " from bundle resources!", HttpStatus.NOT_FOUND_404); + } +} diff --git a/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/manager/ShellyManagerOtaPage.java b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/manager/ShellyManagerOtaPage.java new file mode 100644 index 0000000000000..8ba73b023c57d --- /dev/null +++ b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/manager/ShellyManagerOtaPage.java @@ -0,0 +1,221 @@ +/** + * Copyright (c) 2010-2021 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.manager; + +import static org.openhab.binding.shelly.internal.ShellyBindingConstants.SHELLY_API_TIMEOUT_MS; +import static org.openhab.binding.shelly.internal.manager.ShellyManagerConstants.*; +import static org.openhab.binding.shelly.internal.util.ShellyUtils.*; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; +import java.util.TreeMap; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.api.ContentResponse; +import org.eclipse.jetty.client.api.Request; +import org.eclipse.jetty.http.HttpFields; +import org.eclipse.jetty.http.HttpMethod; +import org.eclipse.jetty.http.HttpStatus; +import org.openhab.binding.shelly.internal.ShellyHandlerFactory; +import org.openhab.binding.shelly.internal.api.ShellyApiException; +import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsUpdate; +import org.openhab.binding.shelly.internal.api.ShellyDeviceProfile; +import org.openhab.binding.shelly.internal.api.ShellyHttpApi; +import org.openhab.binding.shelly.internal.config.ShellyThingConfiguration; +import org.openhab.binding.shelly.internal.handler.ShellyManagerInterface; +import org.openhab.binding.shelly.internal.provider.ShellyTranslationProvider; +import org.openhab.core.thing.ThingStatusDetail; +import org.osgi.service.cm.ConfigurationAdmin; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * {@link ShellyManagerOtaPage} implements the Shelly Manager's download proxy for images (load them from bundle) + * + * @author Markus Michels - Initial contribution + */ +@NonNullByDefault +public class ShellyManagerOtaPage extends ShellyManagerPage { + protected final Logger logger = LoggerFactory.getLogger(ShellyManagerOtaPage.class); + + public ShellyManagerOtaPage(ConfigurationAdmin configurationAdmin, ShellyTranslationProvider translationProvider, + HttpClient httpClient, String localIp, int localPort, ShellyHandlerFactory handlerFactory) { + super(configurationAdmin, translationProvider, httpClient, localIp, localPort, handlerFactory); + } + + @Override + public ShellyMgrResponse generateContent(String path, Map parameters) throws ShellyApiException { + if (path.contains(SHELLY_MGR_OTA_URI)) { + return loadFirmware(path, parameters); + } else { + return generatePage(path, parameters); + } + } + + public ShellyMgrResponse generatePage(String path, Map parameters) throws ShellyApiException { + String uid = getUrlParm(parameters, URLPARM_UID); + String version = getUrlParm(parameters, URLPARM_VERSION); + String update = getUrlParm(parameters, URLPARM_UPDATE); + String connection = getUrlParm(parameters, URLPARM_CONNECTION); + String url = getUrlParm(parameters, URLPARM_URL); + if (uid.isEmpty() || (version.isEmpty() && connection.isEmpty()) || !getThingHandlers().containsKey(uid)) { + return new ShellyMgrResponse("Invalid URL parameters: " + parameters, HttpStatus.BAD_REQUEST_400); + } + + Map properties = new HashMap<>(); + String html = loadHTML(HEADER_HTML, properties); + ShellyManagerInterface th = getThingHandlers().get(uid); + if (th != null) { + properties = fillProperties(new HashMap<>(), uid, th); + ShellyThingConfiguration config = getThingConfig(th, properties); + ShellyDeviceProfile profile = th.getProfile(); + String deviceType = getDeviceType(properties); + + String uri = !url.isEmpty() && connection.equals(CONNECTION_TYPE_CUSTOM) ? url + : getFirmwareUrl(config.deviceIp, deviceType, profile.mode, version, + connection.equals(CONNECTION_TYPE_LOCAL)); + if (connection.equalsIgnoreCase(CONNECTION_TYPE_INTERNET)) { + // If target + // - contains "update=xx" then use -> ?update=true for release and ?beta=true for beta + // - otherwise qualify full url with ?url=xxxx + if (uri.contains("update=") || uri.contains("beta=")) { + url = uri; + } else { + url = URLPARM_URL + "=" + uri; + } + } else if (connection.equalsIgnoreCase(CONNECTION_TYPE_LOCAL)) { + // redirect to local server -> http://:/shelly/manager/ota?deviceType=xxx&version=xxx + String modeParm = !profile.mode.isEmpty() ? "&" + URLPARM_DEVMODE + "=" + profile.mode : ""; + url = URLPARM_URL + "=http://" + localIp + ":" + localPort + SHELLY_MGR_OTA_URI + urlEncode( + "?" + URLPARM_DEVTYPE + "=" + deviceType + modeParm + "&" + URLPARM_VERSION + "=" + version); + } else if (connection.equalsIgnoreCase(CONNECTION_TYPE_CUSTOM)) { + // else custom -> don't modify url + uri = url; + url = URLPARM_URL + "=" + uri; + } + String updateUrl = url; + + properties.put(ATTRIBUTE_VERSION, version); + properties.put(ATTRIBUTE_FW_URL, uri); + properties.put(ATTRIBUTE_UPDATE_URL, "http://" + getDeviceIp(properties) + "/ota?" + updateUrl); + properties.put(URLPARM_CONNECTION, connection); + + if (update.equalsIgnoreCase("yes")) { + // do the update + th.setThingOffline(ThingStatusDetail.FIRMWARE_UPDATING, "offline.status-error-fwupgrade"); + html += loadHTML(FWUPDATE2_HTML, properties); + + new Thread(() -> { // schedule asynchronous reboot + try { + ShellyHttpApi api = new ShellyHttpApi(uid, config, httpClient); + ShellySettingsUpdate result = api.firmwareUpdate(updateUrl); + String status = getString(result.status); + logger.info("{}: {}", th.getThingName(), getMessage("fwupdate.initiated", status)); + + // Shelly Motion needs almost 2min for upgrade + scheduleUpdate(th, uid + "_upgrade", profile.isMotion ? 110 : 30); + } catch (ShellyApiException e) { + // maybe the device restarts before returning the http response + logger.warn("{}: {}", th.getThingName(), getMessage("fwupdate.initiated", e.toString())); + } + }).start(); + } else { + String message = getMessageP("fwupdate.confirm", MCINFO); + properties.put(ATTRIBUTE_MESSAGE, message); + html += loadHTML(FWUPDATE1_HTML, properties); + } + } + + html += loadHTML(FOOTER_HTML, properties); + return new ShellyMgrResponse(html, HttpStatus.OK_200); + } + + protected ShellyMgrResponse loadFirmware(String path, Map parameters) throws ShellyApiException { + String deviceType = getUrlParm(parameters, URLPARM_DEVTYPE); + String deviceMode = getUrlParm(parameters, URLPARM_DEVMODE); + String version = getUrlParm(parameters, URLPARM_VERSION); + String url = getUrlParm(parameters, URLPARM_URL); + logger.info("ShellyManager: {}", getMessage("fwupdate.info", deviceType, version, url)); + + String failure = getMessage("fwupdate.notfound", deviceType, version, url); + try { + if (url.isEmpty()) { + url = getFirmwareUrl("", deviceType, deviceMode, version, true); + if (url.isEmpty()) { + logger.warn("ShellyManager: {}", failure); + return new ShellyMgrResponse(failure, HttpStatus.BAD_REQUEST_400); + } + } + + logger.debug("ShellyManager: Loading firmware from {}", url); + // BufferedInputStream in = new BufferedInputStream(new URL(url).openStream()); + // byte[] buf = new byte[in.available()]; + // in.read(buf); + Request request = httpClient.newRequest(url).method(HttpMethod.GET).timeout(SHELLY_API_TIMEOUT_MS, + TimeUnit.MILLISECONDS); + ContentResponse contentResponse = request.send(); + HttpFields fields = contentResponse.getHeaders(); + Map headers = new TreeMap<>(); + String etag = getString(fields.get("ETag")); + String ranges = getString(fields.get("accept-ranges")); + String modified = getString(fields.get("Last-Modified")); + headers.put("ETag", etag); + headers.put("accept-ranges", ranges); + headers.put("Last-Modified", modified); + byte[] data = contentResponse.getContent(); + logger.info("ShellyManager: {}", getMessage("fwupdate.success", data.length, etag, modified)); + return new ShellyMgrResponse(data, HttpStatus.OK_200, contentResponse.getMediaType(), headers); + } catch (ExecutionException | TimeoutException | InterruptedException | RuntimeException e) { + logger.info("ShellyManager: {}", failure, e); + return new ShellyMgrResponse(failure, HttpStatus.BAD_REQUEST_400); + + } + } + + protected String getFirmwareUrl(String deviceIp, String deviceType, String mode, String version, boolean local) + throws ShellyApiException { + switch (version) { + case FWPROD: + case FWBETA: + boolean prod = version.equals(FWPROD); + if (!local) { + // run regular device update + return prod ? "update=true" : "beta=true"; + } else { + // convert prod/beta to full url + FwRepoEntry fw = getFirmwareRepoEntry(deviceType, mode); + String url = getString(prod ? fw.url : fw.beta_url); + logger.debug("ShellyManager: Map {} release to url {}, version {}", url, + prod ? fw.url : fw.beta_url, prod ? fw.version : fw.beta_ver); + return url; + } + default: // Update from firmware archive + FwArchList list = getFirmwareArchiveList(deviceType); + ArrayList versions = list.versions; + if (versions != null) { + for (FwArchEntry e : versions) { + String url = FWREPO_ARCFILE_URL + version + "/" + getString(e.file); + if (getString(e.version).equalsIgnoreCase(version)) { + return url; + } + } + } + } + return ""; + } +} diff --git a/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/manager/ShellyManagerOverviewPage.java b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/manager/ShellyManagerOverviewPage.java new file mode 100644 index 0000000000000..b7932602c002b --- /dev/null +++ b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/manager/ShellyManagerOverviewPage.java @@ -0,0 +1,307 @@ +/** + * Copyright (c) 2010-2021 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.manager; + +import static org.openhab.binding.shelly.internal.ShellyBindingConstants.*; +import static org.openhab.binding.shelly.internal.api.ShellyDeviceProfile.extractFwVersion; +import static org.openhab.binding.shelly.internal.manager.ShellyManagerConstants.*; +import static org.openhab.binding.shelly.internal.util.ShellyUtils.*; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; +import java.util.TreeMap; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.http.HttpStatus; +import org.openhab.binding.shelly.internal.ShellyHandlerFactory; +import org.openhab.binding.shelly.internal.api.ShellyApiException; +import org.openhab.binding.shelly.internal.api.ShellyDeviceProfile; +import org.openhab.binding.shelly.internal.config.ShellyThingConfiguration; +import org.openhab.binding.shelly.internal.handler.ShellyDeviceStats; +import org.openhab.binding.shelly.internal.handler.ShellyManagerInterface; +import org.openhab.binding.shelly.internal.provider.ShellyTranslationProvider; +import org.openhab.binding.shelly.internal.util.ShellyVersionDTO; +import org.openhab.core.library.types.DecimalType; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.types.State; +import org.openhab.core.types.UnDefType; +import org.osgi.service.cm.ConfigurationAdmin; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * {@link ShellyManagerOtaPage} implements the Shelly Manager's device overview page + * + * @author Markus Michels - Initial contribution + */ +@NonNullByDefault +public class ShellyManagerOverviewPage extends ShellyManagerPage { + private final Logger logger = LoggerFactory.getLogger(ShellyManagerOverviewPage.class); + + public ShellyManagerOverviewPage(ConfigurationAdmin configurationAdmin, + ShellyTranslationProvider translationProvider, HttpClient httpClient, String localIp, int localPort, + ShellyHandlerFactory handlerFactory) { + super(configurationAdmin, translationProvider, httpClient, localIp, localPort, handlerFactory); + } + + @Override + public ShellyMgrResponse generateContent(String path, Map parameters) throws ShellyApiException { + String filter = getUrlParm(parameters, URLPARM_FILTER).toLowerCase(); + String action = getUrlParm(parameters, URLPARM_ACTION).toLowerCase(); + String uidParm = getUrlParm(parameters, URLPARM_UID).toLowerCase(); + + logger.debug("Generating overview for {} devices", getThingHandlers().size()); + + String html = ""; + Map properties = new HashMap<>(); + properties.put(ATTRIBUTE_METATAG, ""); + properties.put(ATTRIBUTE_CSS_HEADER, loadHTML(OVERVIEW_HEADER, properties)); + + String deviceHtml = ""; + TreeMap sortedMap = new TreeMap<>(); + for (Map.Entry th : getThingHandlers().entrySet()) { // sort by Device Name + ShellyManagerInterface handler = th.getValue(); + sortedMap.put(getDisplayName(handler.getThing().getProperties()), handler); + } + + html = loadHTML(HEADER_HTML, properties); + html += loadHTML(OVERVIEW_HTML, properties); + + int filteredDevices = 0; + for (Map.Entry handler : sortedMap.entrySet()) { + try { + ShellyManagerInterface th = handler.getValue(); + ThingStatus status = th.getThing().getStatus(); + ShellyDeviceProfile profile = th.getProfile(); + String uid = getString(th.getThing().getUID().getAsString()); // handler.getKey(); + + if (action.equals(ACTION_REFRESH) && (uidParm.isEmpty() || uidParm.equals(uid))) { + // Refresh thing status, this is asynchronosly and takes 0-3sec + th.requestUpdates(1, true); + } else if (action.equals(ACTION_RES_STATS) && (uidParm.isEmpty() || uidParm.equals(uid))) { + th.resetStats(); + } else if (action.equals(ACTION_OTACHECK) && (uidParm.isEmpty() || uidParm.equals(uid))) { + th.resetStats(); + } + + Map warnings = getStatusWarnings(th); + if (applyFilter(th, filter) || (filter.equals(FILTER_ATTENTION) && !warnings.isEmpty())) { + filteredDevices++; + properties.clear(); + fillProperties(properties, uid, handler.getValue()); + String deviceType = getDeviceType(properties); + + properties.put(ATTRIBUTE_DISPLAY_NAME, getDisplayName(properties)); + properties.put(ATTRIBUTE_DEV_STATUS, fillDeviceStatus(warnings)); + if (!warnings.isEmpty() && (status != ThingStatus.UNKNOWN)) { + properties.put(ATTRIBUTE_STATUS_ICON, ICON_ATTENTION); + } + if (!deviceType.equalsIgnoreCase("unknown") && (status == ThingStatus.ONLINE)) { + properties.put(ATTRIBUTE_FIRMWARE_SEL, fillFirmwareHtml(uid, deviceType, profile.mode)); + properties.put(ATTRIBUTE_ACTION_LIST, fillActionHtml(th, uid)); + } else { + properties.put(ATTRIBUTE_FIRMWARE_SEL, ""); + properties.put(ATTRIBUTE_ACTION_LIST, ""); + } + html += loadHTML(OVERVIEW_DEVICE, properties); + } + } catch (ShellyApiException e) { + logger.debug("{}: Exception", LOG_PREFIX, e); + } + } + + properties.clear(); + properties.put("numberDevices", "" + "Number of devices: " + filteredDevices + + " of " + String.valueOf(getThingHandlers().size()) + " "); + properties.put(ATTRIBUTE_CSS_FOOTER, loadHTML(OVERVIEW_FOOTER, properties)); + html += deviceHtml + loadHTML(FOOTER_HTML, properties); + return new ShellyMgrResponse(fillAttributes(html, properties), HttpStatus.OK_200); + } + + private String fillFirmwareHtml(String uid, String deviceType, String mode) throws ShellyApiException { + String html = "\n\t\t\t\t\n\t\t\t"; + + return html; + } + + private String fillActionHtml(ShellyManagerInterface handler, String uid) { + String html = "\n\t\t\t\t +
+ +
+ + +
+   +
+ + + + + +

+    + +

+

+ + +

+ +

${message}

+ +

+ diff --git a/bundles/org.openhab.binding.shelly/src/main/resources/sniplets/fw_update2.html b/bundles/org.openhab.binding.shelly/src/main/resources/sniplets/fw_update2.html new file mode 100644 index 0000000000000..6d0b63e60b2e3 --- /dev/null +++ b/bundles/org.openhab.binding.shelly/src/main/resources/sniplets/fw_update2.html @@ -0,0 +1,17 @@ +

+

Firmware Update

+

+

Updating device ${deviceName} (${uid}) with version ${version}, connection type=${connection}

+

Update url: ${updateUrl}

+ + +

Wait 1-2 minutes, then check device UI at ${deviceIp}, section Firmware.

+

Do not power-off or restart device while updating the firmware!

+
+ +

+ +

+
+

+ \ No newline at end of file diff --git a/bundles/org.openhab.binding.shelly/src/main/resources/sniplets/header.html b/bundles/org.openhab.binding.shelly/src/main/resources/sniplets/header.html new file mode 100644 index 0000000000000..4205bee9a2e68 --- /dev/null +++ b/bundles/org.openhab.binding.shelly/src/main/resources/sniplets/header.html @@ -0,0 +1,43 @@ + + + + Shelly Manager + + + ${metaTag} + + + + ${cssHeader} + + + diff --git a/bundles/org.openhab.binding.shelly/src/main/resources/sniplets/ov_device.html b/bundles/org.openhab.binding.shelly/src/main/resources/sniplets/ov_device.html new file mode 100644 index 0000000000000..f487a4f0fc5cb --- /dev/null +++ b/bundles/org.openhab.binding.shelly/src/main/resources/sniplets/ov_device.html @@ -0,0 +1,106 @@ + + +

+
+ +
+
+ + + + + + + + + + + ${deviceStatus} +
${thingName}
Status${thingStatus} ${thingStatusDetail}
CoIoT Status${coiotStatus}
CoIoT Destination${coiotDestination}
Cloud Status${cloudStatus}
MQTT Status${mqttStatus}
Actions skipped${actionsSkipped}
Max Internal Temp${maxInternalTemp}
 
+
+
+ + +
+ ${displayName} +
+ + + + + + + + + + + + + + + + +
${thingName}
Shelly Device Name${deviceName}
Device Hardware Rev${deviceHwRev}
Device Type${modelId}
Device Mode${deviceMode}
Firmware Version${firmwareVersion}
Network Name${serviceName}
MAC Address${macAddress}
Discoverable${discoverable}
WiFi Auto Recovery${wifiAutoRecovery}
WiFi AP Roaming${apRoamingMode}
WiFi AP Threshold${apRoamingThreshold}
Timezone${deviceTimezone}
Time Server${sntpServer}
Debug Mode${debugMode}
+
+
+ + +
+ +
+ + +
+ +
+ + +
+ + + +
+ + +
+ + + +
+ + +
+ + + +
+ + ${deviceIp} + ${wifiNetwork} + +
+ Signal quality: ${wifiSignal} (4=best..1=very weak) +
+ +  ${wifiSignalRssi} + ${batteryLevel} + ${heartBeat} + ${actionList} + ${firmwareVersion} + ${updateAvailable} + ${firmwareSelection} + ${uptime} + ${internalTemp} + ${devUpdatePeriod} s + ${remainingWatchdog} s + ${alarmCount} + + ${lastAlarm} + + ${lastAlarmTs} + ${deviceRestarts} + ${timeoutErrors} + ${timeoutsRecovered} + ${coiotMessages} + ${coiotErrors} + diff --git a/bundles/org.openhab.binding.shelly/src/main/resources/sniplets/ov_footer.html b/bundles/org.openhab.binding.shelly/src/main/resources/sniplets/ov_footer.html new file mode 100644 index 0000000000000..b48598c9d3821 --- /dev/null +++ b/bundles/org.openhab.binding.shelly/src/main/resources/sniplets/ov_footer.html @@ -0,0 +1,33 @@ + diff --git a/bundles/org.openhab.binding.shelly/src/main/resources/sniplets/ov_header.html b/bundles/org.openhab.binding.shelly/src/main/resources/sniplets/ov_header.html new file mode 100644 index 0000000000000..e7716f7dff777 --- /dev/null +++ b/bundles/org.openhab.binding.shelly/src/main/resources/sniplets/ov_header.html @@ -0,0 +1,37 @@ + diff --git a/bundles/org.openhab.binding.shelly/src/main/resources/sniplets/overview.html b/bundles/org.openhab.binding.shelly/src/main/resources/sniplets/overview.html new file mode 100644 index 0000000000000..f6ced5fdbe2fa --- /dev/null +++ b/bundles/org.openhab.binding.shelly/src/main/resources/sniplets/overview.html @@ -0,0 +1,60 @@ + + +
+  
+  
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
SName     Device IPWiFi NetworkWiFi SignalBattery LevelHeartbeatActionsFirmwareUpdate availVersionsUptimeInternal TempUpdate PeriodRemaining WatchdogEventsLast EventEvent TimeDevice RestartsTimeout ErrorsTimeouts RecoveredCoIOT MessagesCoIOT Errors