]> git.basschouten.com Git - openhab-addons.git/commitdiff
[shelly] New Shelly Manager (more detailed information, status, integrated firmware...
authorMarkus Michels <markus7017@gmail.com>
Wed, 31 Mar 2021 20:42:33 +0000 (22:42 +0200)
committerGitHub <noreply@github.com>
Wed, 31 Mar 2021 20:42:33 +0000 (22:42 +0200)
* 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 <markus7017@gmail.com>
55 files changed:
bundles/org.openhab.binding.shelly/README.md
bundles/org.openhab.binding.shelly/doc/ShellyManager.md [new file with mode: 0644]
bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/ShellyBindingConstants.java
bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api/ShellyApiJsonDTO.java
bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api/ShellyApiResult.java
bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api/ShellyDeviceProfile.java
bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api/ShellyHttpApi.java
bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/coap/ShellyCoIoTProtocol.java
bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/coap/ShellyCoapHandler.java
bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/handler/ShellyBaseHandler.java
bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/manager/ShellyManager.java [new file with mode: 0644]
bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/manager/ShellyManagerActionPage.java [new file with mode: 0644]
bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/manager/ShellyManagerCache.java [new file with mode: 0644]
bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/manager/ShellyManagerConstants.java [new file with mode: 0644]
bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/manager/ShellyManagerImageLoader.java [new file with mode: 0644]
bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/manager/ShellyManagerOtaPage.java [new file with mode: 0644]
bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/manager/ShellyManagerOverviewPage.java [new file with mode: 0644]
bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/manager/ShellyManagerPage.java [new file with mode: 0644]
bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/manager/ShellyManagerServlet.java [new file with mode: 0644]
bundles/org.openhab.binding.shelly/src/main/resources/OH-INF/i18n/shelly.properties
bundles/org.openhab.binding.shelly/src/main/resources/OH-INF/i18n/shelly_de.properties
bundles/org.openhab.binding.shelly/src/main/resources/images/cloud_connected.png [new file with mode: 0644]
bundles/org.openhab.binding.shelly/src/main/resources/images/cloud_disabled.png [new file with mode: 0644]
bundles/org.openhab.binding.shelly/src/main/resources/images/cloud_enabled.png [new file with mode: 0644]
bundles/org.openhab.binding.shelly/src/main/resources/images/cloud_unknown.png [new file with mode: 0644]
bundles/org.openhab.binding.shelly/src/main/resources/images/mqtt_connected.png [new file with mode: 0644]
bundles/org.openhab.binding.shelly/src/main/resources/images/mqtt_disabled.png [new file with mode: 0644]
bundles/org.openhab.binding.shelly/src/main/resources/images/mqtt_enabled.png [new file with mode: 0644]
bundles/org.openhab.binding.shelly/src/main/resources/images/mqtt_unknown.png [new file with mode: 0644]
bundles/org.openhab.binding.shelly/src/main/resources/images/otacheck.png [new file with mode: 0644]
bundles/org.openhab.binding.shelly/src/main/resources/images/refresh.png [new file with mode: 0644]
bundles/org.openhab.binding.shelly/src/main/resources/images/resetstat.png [new file with mode: 0644]
bundles/org.openhab.binding.shelly/src/main/resources/images/status_attention.png [new file with mode: 0644]
bundles/org.openhab.binding.shelly/src/main/resources/images/status_config.png [new file with mode: 0644]
bundles/org.openhab.binding.shelly/src/main/resources/images/status_initializing.png [new file with mode: 0644]
bundles/org.openhab.binding.shelly/src/main/resources/images/status_offline.png [new file with mode: 0644]
bundles/org.openhab.binding.shelly/src/main/resources/images/status_online.png [new file with mode: 0644]
bundles/org.openhab.binding.shelly/src/main/resources/images/status_uninitialized.png [new file with mode: 0644]
bundles/org.openhab.binding.shelly/src/main/resources/images/status_unknown.png [new file with mode: 0644]
bundles/org.openhab.binding.shelly/src/main/resources/images/wifi.png [new file with mode: 0644]
bundles/org.openhab.binding.shelly/src/main/resources/images/wifi0.png [new file with mode: 0644]
bundles/org.openhab.binding.shelly/src/main/resources/images/wifi1.png [new file with mode: 0644]
bundles/org.openhab.binding.shelly/src/main/resources/images/wifi2.png [new file with mode: 0644]
bundles/org.openhab.binding.shelly/src/main/resources/images/wifi3.png [new file with mode: 0644]
bundles/org.openhab.binding.shelly/src/main/resources/images/wifi4.png [new file with mode: 0644]
bundles/org.openhab.binding.shelly/src/main/resources/sniplets/action.html [new file with mode: 0644]
bundles/org.openhab.binding.shelly/src/main/resources/sniplets/footer.html [new file with mode: 0644]
bundles/org.openhab.binding.shelly/src/main/resources/sniplets/forward.script [new file with mode: 0644]
bundles/org.openhab.binding.shelly/src/main/resources/sniplets/fw_update1.html [new file with mode: 0644]
bundles/org.openhab.binding.shelly/src/main/resources/sniplets/fw_update2.html [new file with mode: 0644]
bundles/org.openhab.binding.shelly/src/main/resources/sniplets/header.html [new file with mode: 0644]
bundles/org.openhab.binding.shelly/src/main/resources/sniplets/ov_device.html [new file with mode: 0644]
bundles/org.openhab.binding.shelly/src/main/resources/sniplets/ov_footer.html [new file with mode: 0644]
bundles/org.openhab.binding.shelly/src/main/resources/sniplets/ov_header.html [new file with mode: 0644]
bundles/org.openhab.binding.shelly/src/main/resources/sniplets/overview.html [new file with mode: 0644]

index dee49a1585f33a66cf60ac8769cffc50ed77d071..459036cb55aa117b3ae1bfe78f74664757c7efcb 100644 (file)
@@ -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 (file)
index 0000000..909d373
--- /dev/null
@@ -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://&lt;openHAB IP address&gt;:8080/shelly/manager or
+- http://&lt;openHAB IP address&gt;: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.<br/>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.
index adb8ab68d76e933f2d9d5d9c3bc9d05ec7765dd5..09ca61a5301ec87d858c5f4cafb248e2c61c939f 100755 (executable)
@@ -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";
index d4be3688a9d26e2188606efcc68155fd33baab5b..ca1d1379a93a49ef27e756f6b6c123641302fbdd 100644 (file)
@@ -543,7 +543,9 @@ public class ShellyApiJsonDTO {
         @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 class ShellyApiJsonDTO {
         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 class ShellyApiJsonDTO {
         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;
index b1a2bc143098d0449b85fb74ddbf37fccc23bd19..4cadd3f96af207a32f658884bb71f4d7fdb33e9a 100644 (file)
@@ -63,6 +63,10 @@ public class ShellyApiResult {
         return httpCode == OK_200;
     }
 
+    public boolean isNotFound() {
+        return httpCode == NOT_FOUND_404;
+    }
+
     public boolean isHttpAccessUnauthorized() {
         return (httpCode == UNAUTHORIZED_401 || response.contains(SHELLY_APIERR_UNAUTHORIZED));
     }
index 2d4a832e6e316291da29e9bcb9a9f8a19c35c0eb..45cc461cf527c97f646e3ffd7154fca6d3691db0 100644 (file)
@@ -29,6 +29,7 @@ import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsIn
 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 @@ import com.google.gson.Gson;
 @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 class ShellyDeviceProfile {
         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 class ShellyDeviceProfile {
 
         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 class ShellyDeviceProfile {
 
     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);
             }
         }
index c57d9ef00a048eb7fc12d7a6fcf0f2f6ade43b8a..df7bd8a9437f9480db62ff7f236b0a3ba3f3a871 100644 (file)
@@ -32,6 +32,7 @@ import org.eclipse.jetty.http.HttpHeader;
 import org.eclipse.jetty.http.HttpMethod;
 import org.eclipse.jetty.http.HttpStatus;
 import org.openhab.binding.shelly.internal.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 class ShellyHttpApi {
         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 class ShellyHttpApi {
                 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 class ShellyHttpApi {
         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 @@ public class ShellyHttpApi {
             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) {
index 7161f413d13bf20b7588b0257395a91a6a21256c..723f7607985bddf2245a1fff71e0a1305c804a9f 100644 (file)
@@ -22,6 +22,7 @@ import java.util.Map;
 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<String, CoIotDescrBlk> blkMap;
     protected final Map<String, CoIotDescrSen> sensorMap;
     private final Gson gson = new GsonBuilder().create();
@@ -68,6 +70,7 @@ public class ShellyCoIoTProtocol {
         this.blkMap = blkMap;
         this.sensorMap = sensorMap;
         this.profile = thingHandler.getProfile();
+        this.api = thingHandler.getApi();
     }
 
     protected boolean handleStatusUpdate(List<CoIotSensor> sensorUpdates, CoIotDescrSen sen, CoIotSensor s,
index 378c5730d2e029528ad8eb5a5a423f97277aad57..a35dc157fa1a90bff95c33149d2581d82bafd366 100644 (file)
@@ -37,6 +37,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
 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<String, CoIotDescrBlk> blkMap = new LinkedHashMap<>();
     private Map<String, CoIotDescrSen> 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 class ShellyCoapHandler implements ShellyCoapListener {
                 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 class ShellyCoapHandler implements ShellyCoapListener {
                 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 @@ public class ShellyCoapHandler implements ShellyCoapListener {
     }
 
     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);
     }
 
index 064621e57b51c13ae394ccb3e07cc9c60545fb47..25d6b43022f15a1476164a12abdc26a90bba2875 100755 (executable)
@@ -32,6 +32,7 @@ import org.eclipse.jetty.client.HttpClient;
 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 @@ public class ShellyBaseHandler extends BaseThingHandler implements ShellyDeviceL
         }
         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 @@ public class ShellyBaseHandler extends BaseThingHandler implements ShellyDeviceL
                 } 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 @@ public class ShellyBaseHandler extends BaseThingHandler implements ShellyDeviceL
                 // 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 @@ public class ShellyBaseHandler extends BaseThingHandler implements ShellyDeviceL
     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 @@ public class ShellyBaseHandler extends BaseThingHandler implements ShellyDeviceL
         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 @@ public class ShellyBaseHandler extends BaseThingHandler implements ShellyDeviceL
             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<String, String> thingProperties = new TreeMap<>();
         for (Map.Entry<String, Object> property : properties.entrySet()) {
@@ -1142,8 +1143,7 @@ public class ShellyBaseHandler extends BaseThingHandler implements ShellyDeviceL
         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 class ShellyBaseHandler extends BaseThingHandler implements ShellyDeviceL
     public Map<String, String> 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 (file)
index 0000000..388b2a3
--- /dev/null
@@ -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<String, ShellyManagerPage> 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<String, String[]> parameters) throws ShellyApiException {
+        for (Map.Entry<String, ShellyManagerPage> 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 (file)
index 0000000..927fbe6
--- /dev/null
@@ -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<String, String[]> 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<String, String> 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<String, String> 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]", "<br>");
+                    } 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<String, String> getActions(ShellyDeviceProfile profile) {
+        Map<String, String> 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 (file)
index 0000000..5af3e0f
--- /dev/null
@@ -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<K, V> extends ConcurrentHashMap<K, V> {
+
+    private static final long serialVersionUID = 1L;
+
+    private Map<K, Long> timeMap = new ConcurrentHashMap<K, Long>();
+    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<? extends K, ? extends V> 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 (file)
index 0000000..5f6151a
--- /dev/null
@@ -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 (file)
index 0000000..dfdad7e
--- /dev/null
@@ -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<String, String[]> 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 (file)
index 0000000..8ba73b0
--- /dev/null
@@ -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<String, String[]> 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<String, String[]> 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<String, String> 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://<oh-ip>:<oh-port>/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<String, String[]> 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<String, String> 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<FwArchEntry> 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 (file)
index 0000000..b793260
--- /dev/null
@@ -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<String, String[]> 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<String, String> properties = new HashMap<>();
+        properties.put(ATTRIBUTE_METATAG, "<meta http-equiv=\"refresh\" content=\"60\" />");
+        properties.put(ATTRIBUTE_CSS_HEADER, loadHTML(OVERVIEW_HEADER, properties));
+
+        String deviceHtml = "";
+        TreeMap<String, ShellyManagerInterface> sortedMap = new TreeMap<>();
+        for (Map.Entry<String, ShellyManagerInterface> 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<String, ShellyManagerInterface> 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<String, String> 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", "<span class=\"footerDevices\">" + "Number of devices: " + filteredDevices
+                + " of " + String.valueOf(getThingHandlers().size()) + "&nbsp;</span>");
+        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<select name=\"fwList\" id=\"fwList\" onchange=\"location = this.options[this.selectedIndex].value;\">\n";
+        html += "\t\t\t\t\t<option value=\"\" selected disabled hidden>update to</option>\n";
+
+        String pVersion = "";
+        String bVersion = "";
+        String updateUrl = SHELLY_MGR_FWUPDATE_URI + "?" + URLPARM_UID + "=" + urlEncode(uid);
+        try {
+            // Get current prod + beta version from original firmware repo
+            logger.debug("{}: Load firmware version list for device type {}", LOG_PREFIX, deviceType);
+            FwRepoEntry fw = getFirmwareRepoEntry(deviceType, mode);
+            pVersion = extractFwVersion(fw.version);
+            if (!pVersion.isEmpty()) {
+                html += "\t\t\t\t\t<option value=\"" + updateUrl + "&" + URLPARM_VERSION + "=" + FWPROD + "\">Release "
+                        + pVersion + "</option>\n";
+            }
+            bVersion = extractFwVersion(fw.beta_ver);
+            if (!bVersion.isEmpty()) {
+                html += "\t\t\t\t\t<option value=\"" + updateUrl + "&" + URLPARM_VERSION + "=" + FWBETA + "\">Beta "
+                        + bVersion + "</option>\n";
+            }
+
+            // Add those from Shelly Firmware Archive
+            String json = httpGet(FWREPO_ARCH_URL + "?" + URLPARM_TYPE + "=" + deviceType);
+            if (json.startsWith("[]")) {
+                // no files available for this device type
+                logger.debug("{}: No firmware files found for device type {}", LOG_PREFIX, deviceType);
+            } else {
+                // Create selection list
+                json = "{" + json.replace("[{", "\"versions\":[{") + "}"; // make it an named array
+                FwArchList list = getFirmwareArchiveList(deviceType);
+                ArrayList<FwArchEntry> versions = list.versions;
+                if (versions != null) {
+                    html += "\t\t\t\t\t<option value=\"\" disabled>-- Archive:</option>\n";
+                    for (int i = versions.size() - 1; i >= 0; i--) {
+                        FwArchEntry e = versions.get(i);
+                        String version = getString(e.version);
+                        ShellyVersionDTO v = new ShellyVersionDTO();
+                        if (!version.equalsIgnoreCase(pVersion) && !version.equalsIgnoreCase(bVersion)
+                                && (v.compare(version, SHELLY_API_MIN_FWCOIOT) >= 0) || version.contains("master")) {
+                            html += "\t\t\t\t\t<option value=\"" + updateUrl + "&" + URLPARM_VERSION + "=" + version
+                                    + "\">" + version + "</option>\n";
+                        }
+                    }
+                }
+            }
+        } catch (ShellyApiException e) {
+            logger.debug("{}: Unable to retrieve firmware list: {}", LOG_PREFIX, e.toString());
+        }
+
+        html += "\t\t\t\t\t<option class=\"select-hr\" value=\"" + SHELLY_MGR_FWUPDATE_URI + "?uid=" + uid
+                + "&connection=custom\">Custom URL</option>\n";
+
+        html += "\t\t\t\t</select>\n\t\t\t";
+
+        return html;
+    }
+
+    private String fillActionHtml(ShellyManagerInterface handler, String uid) {
+        String html = "\n\t\t\t\t<select name=\"actionList\" id=\"actionList\" onchange=\"location = '"
+                + SHELLY_MGR_ACTION_URI + "?uid=" + urlEncode(uid) + "&" + URLPARM_ACTION
+                + "='+this.options[this.selectedIndex].value;\">\n";
+        html += "\t\t\t\t\t<option value=\"\" selected disabled>select</option>\n";
+
+        Map<String, String> actionList = ShellyManagerActionPage.getActions(handler.getProfile());
+        for (Map.Entry<String, String> a : actionList.entrySet()) {
+            String value = a.getValue();
+            String seperator = "";
+            if (value.startsWith("-")) {
+                // seperator = "class=\"select-hr\" ";
+                html += "\t\t\t\t\t<option class=\"select-hr\" role=\"seperator\" disabled>&nbsp;</option>\n";
+                value = substringAfterLast(value, "-");
+            }
+            html += "\t\t\t\t\t<option " + seperator + "value=\"" + a.getKey()
+                    + (value.startsWith(ACTION_NONE) ? " disabled " : "") + "\">" + value + "</option>\n";
+        }
+        html += "\t\t\t\t</select>\n\t\t\t";
+        return html;
+    }
+
+    private boolean applyFilter(ShellyManagerInterface handler, String filter) {
+        ThingStatus status = handler.getThing().getStatus();
+        ShellyDeviceProfile profile = handler.getProfile();
+
+        switch (filter) {
+            case FILTER_ONLINE:
+                return status == ThingStatus.ONLINE;
+            case FILTER_INACTIVE:
+                return status != ThingStatus.ONLINE;
+            case FILTER_ATTENTION:
+                return false;
+            case FILTER_UPDATE:
+                // return handler.getChannelValue(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_UPDATE) == OnOffType.ON;
+                return getBool(profile.status.hasUpdate);
+            case FILTER_UNPROTECTED:
+                return !profile.auth;
+            case "*":
+            default:
+                return true;
+        }
+    }
+
+    private Map<String, String> getStatusWarnings(ShellyManagerInterface handler) {
+        Thing thing = handler.getThing();
+        ThingStatus status = handler.getThing().getStatus();
+        ShellyDeviceStats stats = handler.getStats();
+        ShellyDeviceProfile profile = handler.getProfile();
+        ShellyThingConfiguration config = thing.getConfiguration().as(ShellyThingConfiguration.class);
+        TreeMap<String, String> result = new TreeMap<>();
+
+        if ((status != ThingStatus.ONLINE) && (status != ThingStatus.UNKNOWN)) {
+            result.put("Thing Status", status.toString());
+        }
+        State wifiSignal = handler.getChannelValue(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_RSSI);
+        if ((profile.alwaysOn || (profile.hasBattery && (status == ThingStatus.ONLINE)))
+                && ((wifiSignal != UnDefType.NULL) && (((DecimalType) wifiSignal).intValue() < 2))) {
+            result.put("Weak WiFi Signal", wifiSignal.toString());
+        }
+        if (profile.hasBattery) {
+            State lowBattery = handler.getChannelValue(CHANNEL_GROUP_BATTERY, CHANNEL_SENSOR_BAT_LOW);
+            if ((lowBattery == OnOffType.ON)) {
+                lowBattery = handler.getChannelValue(CHANNEL_GROUP_BATTERY, CHANNEL_SENSOR_BAT_LEVEL);
+                result.put("Battery Low", lowBattery.toString());
+            }
+        }
+
+        if (stats.lastAlarm.equalsIgnoreCase(ALARM_TYPE_RESTARTED)) {
+            result.put("Device Alarm", ALARM_TYPE_RESTARTED + " (" + convertTimestamp(stats.lastAlarmTs) + ")");
+        }
+        if (getBool(profile.status.overtemperature)) {
+            result.put("Device Alarm", ALARM_TYPE_OVERTEMP);
+        }
+        if (getBool(profile.status.overload)) {
+            result.put("Device Alarm", ALARM_TYPE_OVERLOAD);
+        }
+        if (getBool(profile.status.loaderror)) {
+            result.put("Device Alarm", ALARM_TYPE_LOADERR);
+        }
+        if (profile.isSensor) {
+            State sensorError = handler.getChannelValue(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_ERROR);
+            if (sensorError != UnDefType.NULL) {
+                if (!sensorError.toString().isEmpty()) {
+                    result.put("Device Alarm", ALARM_TYPE_SENSOR_ERROR);
+                }
+            }
+        }
+        if (profile.alwaysOn && (status == ThingStatus.ONLINE)) {
+            if ((config.eventsCoIoT) && (profile.settings.coiot != null)) {
+                if ((profile.settings.coiot.enabled != null) && !profile.settings.coiot.enabled) {
+                    result.put("CoIoT Status", "COIOT_DISABLED");
+                } else if (stats.coiotMessages == 0) {
+                    result.put("CoIoT Discovery", "NO_COIOT_DISCOVERY");
+                } else if (stats.coiotMessages < 2) {
+                    result.put("CoIoT Multicast", "NO_COIOT_MULTICAST");
+                }
+            }
+        }
+
+        return result;
+    }
+
+    private String fillDeviceStatus(Map<String, String> devStatus) {
+        if (devStatus.isEmpty()) {
+            return "";
+        }
+
+        String result = "\t\t\t\t<tr><td colspan = \"2\">Notifications:</td></tr>";
+        for (Map.Entry<String, String> ds : devStatus.entrySet()) {
+            result += "\t\t\t\t<tr><td>" + ds.getKey() + "</td><td>" + ds.getValue() + "</td></tr>\n";
+        }
+        return result;
+    }
+}
diff --git a/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/manager/ShellyManagerPage.java b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/manager/ShellyManagerPage.java
new file mode 100644 (file)
index 0000000..cbe79cb
--- /dev/null
@@ -0,0 +1,591 @@
+/**
+ * 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.manager.ShellyManagerConstants.*;
+import static org.openhab.binding.shelly.internal.util.ShellyUtils.*;
+import static org.openhab.core.thing.Thing.*;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Timer;
+import java.util.TimerTask;
+import java.util.TreeMap;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.stream.Collectors;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.jetty.client.HttpClient;
+import org.eclipse.jetty.client.api.ContentResponse;
+import org.eclipse.jetty.client.api.Request;
+import org.eclipse.jetty.http.HttpHeader;
+import org.eclipse.jetty.http.HttpMethod;
+import org.eclipse.jetty.http.HttpStatus;
+import org.openhab.binding.shelly.internal.ShellyHandlerFactory;
+import org.openhab.binding.shelly.internal.api.ShellyApiException;
+import org.openhab.binding.shelly.internal.api.ShellyApiResult;
+import org.openhab.binding.shelly.internal.api.ShellyDeviceProfile;
+import org.openhab.binding.shelly.internal.api.ShellyHttpApi;
+import org.openhab.binding.shelly.internal.config.ShellyBindingConfiguration;
+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.core.library.types.DateTimeType;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.thing.ThingStatusDetail;
+import org.openhab.core.types.State;
+import org.openhab.core.types.UnDefType;
+import org.osgi.service.cm.Configuration;
+import org.osgi.service.cm.ConfigurationAdmin;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.gson.Gson;
+
+/**
+ * {@link ShellyManagerOtaPage} implements the Shelly Manager's page template
+ *
+ * @author Markus Michels - Initial contribution
+ */
+@NonNullByDefault
+public class ShellyManagerPage {
+    private final Logger logger = LoggerFactory.getLogger(ShellyManagerPage.class);
+    protected final ShellyTranslationProvider resources;
+
+    private final ShellyHandlerFactory handlerFactory;
+    protected final HttpClient httpClient;
+    protected final ConfigurationAdmin configurationAdmin;
+    protected final ShellyBindingConfiguration bindingConfig = new ShellyBindingConfiguration();
+    protected final String localIp;
+    protected final int localPort;
+
+    protected final Map<String, String> htmlTemplates = new HashMap<>();
+    protected final Gson gson = new Gson();
+
+    protected final ShellyManagerCache<String, FwRepoEntry> firmwareRepo = new ShellyManagerCache<>(15 * 60 * 1000);
+    protected final ShellyManagerCache<String, FwArchList> firmwareArch = new ShellyManagerCache<>(15 * 60 * 1000);
+
+    public static class ShellyMgrResponse {
+        public @Nullable Object data = "";
+        public String mimeType = "";
+        public String redirectUrl = "";
+        public int code;
+        public Map<String, String> headers = new HashMap<>();
+
+        public ShellyMgrResponse() {
+            init("", HttpStatus.OK_200, "text/html", null);
+        }
+
+        public ShellyMgrResponse(Object data, int code) {
+            init(data, code, "text/html", null);
+        }
+
+        public ShellyMgrResponse(Object data, int code, String mimeType) {
+            init(data, code, mimeType, null);
+        }
+
+        public ShellyMgrResponse(Object data, int code, String mimeType, Map<String, String> headers) {
+            init(data, code, mimeType, headers);
+        }
+
+        private void init(Object message, int code, String mimeType, @Nullable Map<String, String> headers) {
+            this.data = message;
+            this.code = code;
+            this.mimeType = mimeType;
+            this.headers = headers != null ? headers : new TreeMap<>();
+        }
+
+        public void setRedirect(String redirectUrl) {
+            this.redirectUrl = redirectUrl;
+        }
+    }
+
+    public static class FwArchEntry {
+        // {"version":"v1.5.10","file":"SHSW-1.zip"}
+        public @Nullable String version;
+        public @Nullable String file;
+    }
+
+    public static class FwArchList {
+        public @Nullable ArrayList<FwArchEntry> versions;
+    }
+
+    public static class FwRepoEntry {
+        public @Nullable String url; // prod
+        public @Nullable String version;
+
+        public @Nullable String beta_url; // beta version if avilable
+        public @Nullable String beta_ver;
+    }
+
+    public ShellyManagerPage(ConfigurationAdmin configurationAdmin, ShellyTranslationProvider translationProvider,
+            HttpClient httpClient, String localIp, int localPort, ShellyHandlerFactory handlerFactory) {
+        this.configurationAdmin = configurationAdmin;
+        this.resources = translationProvider;
+        this.handlerFactory = handlerFactory;
+        this.httpClient = httpClient;
+        this.localIp = localIp;
+        this.localPort = localPort;
+    }
+
+    public ShellyMgrResponse generateContent(String path, Map<String, String[]> parameters) throws ShellyApiException {
+        return new ShellyMgrResponse("Invalid Request", HttpStatus.BAD_REQUEST_400);
+    }
+
+    protected String loadHTML(String template) throws ShellyApiException {
+        if (htmlTemplates.containsKey(template)) {
+            return getString(htmlTemplates.get(template));
+        }
+
+        String html = "";
+        String file = TEMPLATE_PATH + template;
+        logger.debug("Read HTML from {}", file);
+        ClassLoader cl = ShellyManagerInterface.class.getClassLoader();
+        if (cl != null) {
+            try (InputStream inputStream = cl.getResourceAsStream(file)) {
+                if (inputStream != null) {
+                    html = new BufferedReader(new InputStreamReader(inputStream)).lines()
+                            .collect(Collectors.joining("\n"));
+                    htmlTemplates.put(template, html);
+                }
+            } catch (IOException e) {
+                throw new ShellyApiException("Unable to read " + file + " from bundle resources!", e);
+            }
+        }
+        return html;
+    }
+
+    protected String loadHTML(String template, Map<String, String> properties) throws ShellyApiException {
+        properties.put(ATTRIBUTE_URI, SHELLY_MANAGER_URI);
+        String html = loadHTML(template);
+        return fillAttributes(html, properties);
+    }
+
+    protected Map<String, String> fillProperties(Map<String, String> properties, String uid,
+            ShellyManagerInterface th) {
+        try {
+            Configuration serviceConfig = configurationAdmin.getConfiguration("binding." + BINDING_ID);
+            bindingConfig.updateFromProperties(serviceConfig.getProperties());
+        } catch (IOException e) {
+            logger.debug("ShellyManager: Unable to get bindingConfig");
+        }
+
+        properties.putAll(th.getThing().getProperties());
+
+        Thing thing = th.getThing();
+        ThingStatus status = thing.getStatus();
+        properties.put("thingName", getString(thing.getLabel()));
+        properties.put("thingStatus", status.toString());
+        ThingStatusDetail detail = thing.getStatusInfo().getStatusDetail();
+        properties.put("thingStatusDetail", detail.equals(ThingStatusDetail.NONE) ? "" : getString(detail.toString()));
+        properties.put("thingStatusDescr", getString(thing.getStatusInfo().getDescription()));
+        properties.put(ATTRIBUTE_UID, uid);
+
+        ShellyDeviceProfile profile = th.getProfile();
+        ShellyThingConfiguration config = thing.getConfiguration().as(ShellyThingConfiguration.class);
+        ShellyDeviceStats stats = th.getStats();
+        properties.putAll(stats.asProperties());
+
+        for (Map.Entry<String, Object> p : thing.getConfiguration().getProperties().entrySet()) {
+            String key = p.getKey();
+            if (p.getValue() != null) {
+                String value = p.getValue().toString();
+                properties.put(key, value);
+            }
+        }
+
+        State state = th.getChannelValue(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_NAME);
+        if (state != UnDefType.NULL) {
+            addAttribute(properties, th, CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_NAME);
+        } else {
+            // If the Shelly doesn't provide a device name (not configured) we use the service name
+            String deviceName = getDeviceName(properties);
+            properties.put(PROPERTY_DEV_NAME,
+                    !deviceName.isEmpty() ? deviceName : getString(properties.get(PROPERTY_SERVICE_NAME)));
+        }
+
+        if (config.userId.isEmpty()) {
+            // Get defauls from Binding Config
+            properties.put("userId", bindingConfig.defaultUserId);
+            properties.put("password", bindingConfig.defaultPassword);
+        }
+
+        addAttribute(properties, th, CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_RSSI);
+        addAttribute(properties, th, CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_UPTIME);
+        addAttribute(properties, th, CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_HEARTBEAT);
+        addAttribute(properties, th, CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_ITEMP);
+        addAttribute(properties, th, CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_WAKEUP);
+        addAttribute(properties, th, CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_CHARGER);
+        addAttribute(properties, th, CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_UPDATE);
+        addAttribute(properties, th, CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_ALARM);
+        addAttribute(properties, th, CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_CHARGER);
+
+        properties.put(ATTRIBUTE_DEBUG_MODE, getOption(profile.settings.debug_enable));
+        properties.put(ATTRIBUTE_DISCOVERABLE, String.valueOf(getBool(profile.settings.discoverable)));
+        properties.put(ATTRIBUTE_WIFI_RECOVERY, String.valueOf(getBool(profile.settings.wifiRecoveryReboot)));
+        properties.put(ATTRIBUTE_APR_MODE,
+                profile.settings.apRoaming != null ? getOption(profile.settings.apRoaming.enabled) : "n/a");
+        properties.put(ATTRIBUTE_APR_TRESHOLD,
+                profile.settings.apRoaming != null ? getOption(profile.settings.apRoaming.threshold) : "n/a");
+        properties.put(ATTRIBUTE_PWD_PROTECT,
+                profile.auth ? "enabled, user=" + getString(profile.settings.login.username) : "disabled");
+        String tz = getString(profile.settings.timezone);
+        properties.put(ATTRIBUTE_TIMEZONE,
+                (tz.isEmpty() ? "n/a" : tz) + ", auto-detect: " + getBool(profile.settings.tzautodetect));
+        properties.put(ATTRIBUTE_ACTIONS_SKIPPED,
+                profile.status.astats != null ? String.valueOf(profile.status.astats.skipped) : "n/a");
+        properties.put(ATTRIBUTE_MAX_ITEMP, stats.maxInternalTemp > 0 ? stats.maxInternalTemp + " Â°C" : "n/a");
+
+        // Shelly H&T: When external power is connected the battery level is not valid
+        if (!profile.isHT || (getInteger(profile.settings.externalPower) == 0)) {
+            addAttribute(properties, th, CHANNEL_GROUP_BATTERY, CHANNEL_SENSOR_BAT_LEVEL);
+        } else {
+            properties.put(CHANNEL_SENSOR_BAT_LEVEL, "USB");
+        }
+
+        String wiFiSignal = getString(properties.get(CHANNEL_DEVST_RSSI));
+        if (!wiFiSignal.isEmpty()) {
+            properties.put("wifiSignalRssi", wiFiSignal + " / " + stats.wifiRssi + " dBm");
+            properties.put("imgWiFi", "imgWiFi" + wiFiSignal);
+        }
+
+        if (profile.settings.sntp != null) {
+            properties.put(ATTRIBUTE_SNTP_SERVER,
+                    getString(profile.settings.sntp.server) + ", enabled: " + getBool((profile.settings.sntp.enabled)));
+        }
+
+        boolean coiotEnabled = true;
+        if ((profile.settings.coiot != null) && (profile.settings.coiot.enabled != null)) {
+            coiotEnabled = profile.settings.coiot.enabled;
+        }
+        properties.put(ATTRIBUTE_COIOT_STATUS,
+                !coiotEnabled ? "Disbaled in settings" : "Events are " + (config.eventsCoIoT ? "enabled" : "disabled"));
+        properties.put(ATTRIBUTE_COIOT_PEER,
+                (profile.settings.coiot != null) && !getString(profile.settings.coiot.peer).isEmpty()
+                        ? profile.settings.coiot.peer
+                        : "Multicast");
+        if (profile.status.cloud != null) {
+            properties.put(ATTRIBUTE_CLOUD_STATUS,
+                    getBool(profile.settings.cloud.enabled)
+                            ? getBool(profile.status.cloud.connected) ? "connected" : "enabled"
+                            : "disabled");
+        } else {
+            properties.put(ATTRIBUTE_CLOUD_STATUS, "unknown");
+        }
+        if (profile.status.mqtt != null) {
+            properties.put(ATTRIBUTE_MQTT_STATUS,
+                    getBool(profile.settings.mqtt.enable)
+                            ? getBool(profile.status.mqtt.connected) ? "connected" : "enabled"
+                            : "disabled");
+        } else {
+            properties.put(ATTRIBUTE_MQTT_STATUS, "unknown");
+        }
+
+        String statusIcon = "";
+        ThingStatus ts = th.getThing().getStatus();
+        switch (ts) {
+            case UNINITIALIZED:
+            case REMOVED:
+            case REMOVING:
+                statusIcon = ICON_UNINITIALIZED;
+                break;
+            case OFFLINE:
+                ThingStatusDetail sd = th.getThing().getStatusInfo().getStatusDetail();
+                if (uid.contains(THING_TYPE_SHELLYUNKNOWN_STR) || (sd == ThingStatusDetail.CONFIGURATION_ERROR)
+                        || (sd == ThingStatusDetail.HANDLER_CONFIGURATION_PENDING)) {
+                    statusIcon = ICON_CONFIG;
+                    break;
+                }
+            default:
+                statusIcon = ts.toString();
+        }
+        properties.put(ATTRIBUTE_STATUS_ICON, statusIcon.toLowerCase());
+
+        return properties;
+    }
+
+    private void addAttribute(Map<String, String> properties, ShellyManagerInterface thingHandler, String group,
+            String attribute) {
+        State state = thingHandler.getChannelValue(group, attribute);
+        String value = "";
+        if (state != UnDefType.NULL) {
+            if (state instanceof DateTimeType) {
+                DateTimeType dt = (DateTimeType) state;
+                switch (attribute) {
+                    case ATTRIBUTE_LAST_ALARM:
+                        value = dt.format(null).replace('T', ' ').replace('-', '/');
+                        break;
+                    default:
+                        value = getTimestamp(dt);
+                        value = dt.format(null).replace('T', ' ').replace('-', '/');
+                }
+            } else {
+                value = state.toString();
+            }
+        }
+        properties.put(attribute, value);
+    }
+
+    protected String fillAttributes(String template, Map<String, String> properties) {
+        if (!template.contains("${")) {
+            // no replacement necessary
+            return template;
+        }
+
+        String result = template;
+        for (Map.Entry<String, String> var : properties.entrySet()) {
+            result = result.replaceAll(java.util.regex.Pattern.quote("${" + var.getKey() + "}"),
+                    getValue(properties, var.getKey()));
+        }
+
+        if (result.contains("${")) {
+            return result.replaceAll("\\Q${\\E.*}", "");
+        } else {
+            return result;
+        }
+    }
+
+    protected String getValue(Map<String, String> properties, String attribute) {
+        String value = getString(properties.get(attribute));
+        if (!value.isEmpty()) {
+            switch (attribute) {
+                case PROPERTY_FIRMWARE_VERSION:
+                    value = substringBeforeLast(value, "-");
+                    break;
+                case PROPERTY_UPDATE_AVAILABLE:
+                    value = value.replace(OnOffType.ON.toString(), "yes");
+                    value = value.replace(OnOffType.OFF.toString(), "no");
+                    break;
+                case CHANNEL_DEVST_HEARTBEAT:
+                    break;
+            }
+        }
+        return value;
+    }
+
+    protected FwRepoEntry getFirmwareRepoEntry(String deviceType, String mode) throws ShellyApiException {
+        logger.debug("ShellyManager: Load firmware list from {}", FWREPO_PROD_URL);
+        FwRepoEntry fw = null;
+        if (firmwareRepo.containsKey(deviceType)) {
+            fw = firmwareRepo.get(deviceType);
+        }
+        String json = httpGet(FWREPO_PROD_URL); // returns a strange JSON format so we are parsing this manually
+        String entry = substringBetween(json, "\"" + deviceType + "\":{", "}");
+        if (!entry.isEmpty()) {
+            entry = "{" + entry + "}";
+            /*
+             * Example:
+             * "SHPLG-1":{
+             * "url":"http:\/\/repo.shelly.cloud\/firmware\/SHPLG-1.zip",
+             * "version":"20201228-092318\/v1.9.3@ad2bb4e3",
+             * "beta_url":"http:\/\/repo.shelly.cloud\/firmware\/rc\/SHPLG-1.zip",
+             * "beta_ver":"20201223-093703\/v1.9.3-rc5@3f583801"
+             * },
+             */
+            fw = fromJson(gson, entry, FwRepoEntry.class);
+
+            // Special case: RGW2 has a split firmware - xxx-white.zip vs. xxx-color.zip
+            if (!mode.isEmpty() && deviceType.equalsIgnoreCase(SHELLYDT_RGBW2)) {
+                // check for spilt firmware
+                String url = substringBefore(fw.url, ".zip") + "-" + mode + ".zip";
+                if (testUrl(url)) {
+                    fw.url = url;
+                    logger.debug("ShellyManager: Release Split-URL for device type {} is {}", deviceType, url);
+                }
+                url = substringBefore(fw.beta_url, ".zip") + "-" + mode + ".zip";
+                if (testUrl(url)) {
+                    fw.beta_url = url;
+                    logger.debug("ShellyManager: Beta Split-URL for device type {} is {}", deviceType, url);
+                }
+            }
+
+            firmwareRepo.put(deviceType, fw);
+        }
+
+        return fw != null ? fw : new FwRepoEntry();
+    }
+
+    protected FwArchList getFirmwareArchiveList(String deviceType) throws ShellyApiException {
+        FwArchList list;
+        String json = "";
+
+        if (firmwareArch.contains(deviceType)) {
+            list = firmwareArch.get(deviceType); // return from cache
+            if (list != null) {
+                return list;
+            }
+        }
+
+        try {
+            if (!deviceType.isEmpty()) {
+                json = httpGet(FWREPO_ARCH_URL + "?type=" + deviceType);
+            }
+        } catch (ShellyApiException e) {
+            logger.debug("{}: Unable to get firmware list for device type {}: {}", LOG_PREFIX, deviceType,
+                    e.toString());
+        }
+        if (json.isEmpty() || json.startsWith("[]")) {
+            // no files available for this device type
+            logger.info("{}: No firmware files found for device type {}", LOG_PREFIX, deviceType);
+            list = new FwArchList();
+            list.versions = new ArrayList<FwArchEntry>();
+        } else {
+            // Create selection list
+            json = "{" + json.replace("[{", "\"versions\":[{") + "}"; // make it an named array
+            list = fromJson(gson, json, FwArchList.class);
+        }
+
+        // save list to cache
+        firmwareArch.put(deviceType, list);
+        return list;
+    }
+
+    protected boolean testUrl(String url) {
+        try {
+            if (url.isEmpty()) {
+                return false;
+            }
+            httpHeadl(url); // causes exception on 404
+            return true;
+        } catch (ShellyApiException e) {
+        }
+        return false;
+    }
+
+    protected String httpGet(String url) throws ShellyApiException {
+        return httpRequest(HttpMethod.GET, url);
+    }
+
+    protected String httpHeadl(String url) throws ShellyApiException {
+        return httpRequest(HttpMethod.HEAD, url);
+    }
+
+    protected String httpRequest(HttpMethod method, String url) throws ShellyApiException {
+        ShellyApiResult apiResult = new ShellyApiResult();
+
+        try {
+            Request request = httpClient.newRequest(url).method(method).timeout(SHELLY_API_TIMEOUT_MS,
+                    TimeUnit.MILLISECONDS);
+            request.header(HttpHeader.ACCEPT, ShellyHttpApi.CONTENT_TYPE_JSON);
+            logger.trace("{}: HTTP {} {}", LOG_PREFIX, method, url);
+            ContentResponse contentResponse = request.send();
+            apiResult = new ShellyApiResult(contentResponse);
+            String response = contentResponse.getContentAsString().replace("\t", "").replace("\r\n", "").trim();
+            logger.trace("{}: HTTP Response {}: {}", LOG_PREFIX, contentResponse.getStatus(), response);
+
+            // validate response, API errors are reported as Json
+            if (contentResponse.getStatus() != HttpStatus.OK_200) {
+                throw new ShellyApiException(apiResult);
+            }
+            return response;
+        } catch (ExecutionException | TimeoutException | InterruptedException | IllegalArgumentException e) {
+            throw new ShellyApiException("HTTP GET failed", e);
+        }
+    }
+
+    protected String getUrlParm(Map<String, String[]> parameters, String param) {
+        String[] p = parameters.get(param);
+        String value = "";
+        if (p != null) {
+            value = getString(p[0]);
+        }
+        return value;
+    }
+
+    protected String getMessage(String key, Object... arguments) {
+        return resources.get("manager." + key, arguments);
+    }
+
+    protected String getMessageP(String key, String msgClass, Object... arguments) {
+        return "<p class=\"" + msgClass + "\">" + getMessage(key, arguments) + "</p>\n";
+    }
+
+    protected String getMessageS(String key, String msgClass, Object... arguments) {
+        return "<span class=\"" + msgClass + "\">" + getMessage(key, arguments) + "</span>\n";
+    }
+
+    protected static String getDeviceType(Map<String, String> properties) {
+        return getString(properties.get(PROPERTY_MODEL_ID));
+    }
+
+    protected static String getDeviceIp(Map<String, String> properties) {
+        return getString(properties.get("deviceIp"));
+    }
+
+    protected static String getDeviceName(Map<String, String> properties) {
+        return getString(properties.get(PROPERTY_DEV_NAME));
+    }
+
+    protected static String getOption(@Nullable Boolean option) {
+        if (option == null) {
+            return "n/a";
+        }
+        return option ? "enabled" : "disabled";
+    }
+
+    protected static String getOption(@Nullable Integer option) {
+        if (option == null) {
+            return "n/a";
+        }
+        return option.toString();
+    }
+
+    protected static String getDisplayName(Map<String, String> properties) {
+        String name = getString(properties.get(PROPERTY_DEV_NAME));
+        if (name.isEmpty()) {
+            name = getString(properties.get(PROPERTY_SERVICE_NAME));
+        }
+        return name;
+    }
+
+    protected ShellyThingConfiguration getThingConfig(ShellyManagerInterface th, Map<String, String> properties) {
+        Thing thing = th.getThing();
+        ShellyThingConfiguration config = thing.getConfiguration().as(ShellyThingConfiguration.class);
+        if (config.userId.isEmpty()) {
+            config.userId = getString(properties.get("userId"));
+            config.password = getString(properties.get("password"));
+        }
+        return config;
+    }
+
+    protected void scheduleUpdate(ShellyManagerInterface th, String name, int delay) {
+        TimerTask task = new TimerTask() {
+            @Override
+            public void run() {
+                th.requestUpdates(1, true);
+            }
+        };
+        Timer timer = new Timer(name);
+        timer.schedule(task, delay * 1000);
+    }
+
+    protected Map<String, ShellyManagerInterface> getThingHandlers() {
+        return handlerFactory.getThingHandlers();
+    }
+
+    protected @Nullable ShellyManagerInterface getThingHandler(String uid) {
+        return getThingHandlers().get(uid);
+    }
+}
diff --git a/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/manager/ShellyManagerServlet.java b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/manager/ShellyManagerServlet.java
new file mode 100644 (file)
index 0000000..d6083cd
--- /dev/null
@@ -0,0 +1,150 @@
+/**
+ * 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 static org.openhab.binding.shelly.internal.util.ShellyUtils.*;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.PrintWriter;
+import java.util.Map;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.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.openhab.core.io.net.http.HttpClientFactory;
+import org.openhab.core.net.HttpServiceUtil;
+import org.openhab.core.net.NetworkAddressService;
+import org.osgi.service.cm.ConfigurationAdmin;
+import org.osgi.service.component.ComponentContext;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.ConfigurationPolicy;
+import org.osgi.service.component.annotations.Deactivate;
+import org.osgi.service.component.annotations.Reference;
+import org.osgi.service.http.HttpService;
+import org.osgi.service.http.NamespaceException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * {@link ShellyManagerServlet} implements the Shelly Manager - a simple device overview/management
+ *
+ * @author Markus Michels - Initial contribution
+ */
+@NonNullByDefault
+@Component(service = HttpServlet.class, configurationPolicy = ConfigurationPolicy.OPTIONAL)
+public class ShellyManagerServlet extends HttpServlet {
+    private static final long serialVersionUID = 1393403713585449126L;
+    private final Logger logger = LoggerFactory.getLogger(ShellyManagerServlet.class);
+
+    private static final String SERVLET_URI = SHELLY_MANAGER_URI;
+    private final ShellyManager manager;
+    private final String className;
+
+    private final HttpService httpService;
+
+    @Activate
+    public ShellyManagerServlet(@Reference ConfigurationAdmin configurationAdmin,
+            @Reference NetworkAddressService networkAddressService, @Reference HttpService httpService,
+            @Reference HttpClientFactory httpClientFactory, @Reference ShellyHandlerFactory handlerFactory,
+            @Reference ShellyTranslationProvider translationProvider, ComponentContext componentContext,
+            Map<String, Object> config) {
+        className = substringAfterLast(getClass().toString(), ".");
+        this.httpService = httpService;
+        String localIp = getString(networkAddressService.getPrimaryIpv4HostAddress());
+        int localPort = HttpServiceUtil.getHttpServicePort(componentContext.getBundleContext());
+        this.manager = new ShellyManager(configurationAdmin, translationProvider,
+                httpClientFactory.getCommonHttpClient(), localIp, localPort, handlerFactory);
+
+        try {
+            httpService.registerServlet(SERVLET_URI, this, null, httpService.createDefaultHttpContext());
+            logger.debug("{}: Started at '{}'", className, SERVLET_URI);
+        } catch (NamespaceException | ServletException | IllegalArgumentException e) {
+            logger.warn("{}: Unable to initialize bindingConfig", className, e);
+        }
+    }
+
+    @Deactivate
+    protected void deactivate() {
+        httpService.unregister(SERVLET_URI);
+        logger.debug("{} stopped", className);
+    }
+
+    @Override
+    protected void service(@Nullable HttpServletRequest request, @Nullable HttpServletResponse response)
+            throws ServletException, IOException, IllegalArgumentException {
+        if ((request == null) || (response == null)) {
+            logger.debug("request or resp must not be null!");
+            return;
+        }
+
+        String path = getString(request.getRequestURI()).toLowerCase();
+        String ipAddress = request.getHeader("HTTP_X_FORWARDED_FOR");
+        ShellyMgrResponse output = new ShellyMgrResponse();
+        PrintWriter print = null;
+        OutputStream bin = null;
+        try {
+            if (ipAddress == null) {
+                ipAddress = request.getRemoteAddr();
+            }
+            Map<String, String[]> parameters = request.getParameterMap();
+            logger.debug("{}: {} Request from {}:{}{}?{}", className, request.getProtocol(), ipAddress,
+                    request.getRemotePort(), path, parameters.toString());
+            if (!path.toLowerCase().startsWith(SERVLET_URI)) {
+                logger.warn("{} received unknown request: path = {}", className, path);
+                return;
+            }
+
+            output = manager.generateContent(path, parameters);
+            response.setContentType(output.mimeType);
+            if (output.mimeType.equals("text/html")) {
+                // Make sure it's UTF-8 encoded
+                response.setCharacterEncoding(UTF_8);
+                print = response.getWriter();
+                print.write((String) output.data);
+            } else {
+                // binary data
+                byte[] data = (byte[]) output.data;
+                response.setContentLength(data.length);
+                bin = response.getOutputStream();
+                bin.write(data, 0, data.length);
+            }
+        } catch (ShellyApiException | RuntimeException e) {
+            logger.debug("{}: Exception uri={}, parameters={}", className, path, request.getParameterMap().toString(),
+                    e);
+            response.setContentType("text/html");
+            print = response.getWriter();
+            print.write("Exception:" + e.toString() + "<br/>Check openHAB.log for details."
+                    + "<p/><a href=\"/shelly/manager\">Return to Overview</a>");
+            logger.debug("{}: {}", className, output);
+            response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+        } finally {
+            if (print != null) {
+                print.close();
+            }
+            if (bin != null) {
+                bin.close();
+            }
+        }
+    }
+}
index e4b70f7aaa1cfd8a5d141b328f10738dc3b2a15b..07e5d570bde58f0e19017b7fc6c9ae4fe99c73b3 100644 (file)
@@ -21,8 +21,8 @@ offline.status-error-restarted = The device has restarted and will be re-initial
 offline.status-error-fwupgrade = Firmware upgrade in progress
 
 message.versioncheck.failed = Unable to check firmware version: {0}
-message.versioncheck.beta = Device is running a Beta version: {0}/{1} ({2}),make sure this is newer than {3} release build.
-message.versioncheck.tooold = WARNING: Firmware might be too old, installed: {0}/{1} ({2}), required minimal {3}.
+message.versioncheck.beta = Device is running a Beta version: {0}/{1}.
+message.versioncheck.tooold = WARNING: Firmware might be too old, installed: {0}/{1}, required minimal {3}.
 message.versioncheck.update = INFO: New firmware available: current version: {0}, new version: {1}
 message.versioncheck.autocoiot = INFO: Firmware is full-filling the minimum version to auto-enable CoIoT
 message.roller.calibrating = Device is not calibrated, use Shelly App to perform initial roller calibration.
@@ -71,3 +71,64 @@ channel-type.shelly.ledPowerDisable.label = Disable Power LED
 channel-type.shelly.ledPowerDisable.description = ON: The power status LED will be deactivated
 channel-type.shelly.ledStatusDisable.label = Disable Status LED
 channel-type.shelly.ledStatusDisable.description = ON: The WiFi status LED will be deactivated
+
+# Shelly Manager
+message.manager.invalid-url = Invalid URL or syntax
+
+message.manager.buttons.ok = OK
+message.manager.buttons.abort = Abort
+
+message.manager.action.unknown = Action {0} is unknown 
+message.manager.action.reset-stats = Reset Statistics
+message.manager.action.restart = Reboot Device
+message.manager.action.restart.info = The device is restarting and reconnects to WiFi. It will take a moment until device status is refreshed in openHAB.
+message.manager.action.restart.confirm = The device will restart and reconnects to WiFi.
+message.manager.action.resstats.confirm = Device statistics and alarm has been reset.
+message.manager.action.setcloud.config = Cloud function is now {0}.
+message.manager.action.protect = Protect Device
+message.manager.action.protect.id-missing = Credentials for device access are not configured, go to Shelly Binding Settings and provide user id and password.<br/>You could use the 'Protect' action to apply this configuration to the device.
+message.manager.action.protect.status = Device protection is currently {0}. User id {1} is required to access the device.
+message.manager.action.protect.new = Device login will be set to user {0} with password {1}.
+message.manager.action.protect.confirm = Device login was updated to user {0} with password {1}.
+message.manager.action.could-enable = Enable Cloud
+message.manager.action.could-disable = Disable Cloud
+message.manager.action.coiot-mcast = Set CoIoT Multicast
+message.manager.action.coiot-peer = Set CoIoT Peer
+message.manager.action.timezone = Set Timezone
+message.manager.action.reset = Factory Reset
+message.manager.action.reset.warning = Attention: Performing this action will reset the device to factory defaults.<br/>All configuration data incl. WiFi settings get lost and device will return to Access Point mode (WiFi {0})
+message.manager.action.reset.confirm = Factory reset was performed. Connect to WiFi network {0} and open http://192.168.33.1 to restart with device setup.
+message.manager.action.checkupd.new = Firmware update available: {0}
+message.manager.action.checkupd.ok = Firmware check completed, check device overview for new version.
+message.manager.action.checkupd.runnuing = Firmware check was initiated.
+message.manager.action.checkupd.failed = Unable to check for firmware update: {0}
+message.manager.action.setwifirec-enable = The device performs an auto-restart if WiFi Recovery Mode is enabled and device is facing WiFi connectivity issues. 
+message.manager.action.setwifirec-disable = WiFi Recovery Mode will be disabled. 
+message.manager.action.setwifirec-confirm = WiFi Recovery Mode has been {0}.
+message.manager.action.setwifirec-failed = Unable to update setting for WiFi Recovery Mode: {0}
+message.manager.action.aproaming-enable = WiFi Access Point Roaming will be enabled. Check product documentation for details. 
+message.manager.action.aproaming-disable = WiFi Access Point Roaming will be disabled. 
+message.manager.action.aproaming-confirm = Unable to update setting WiFi Access Point Roaming: {0}
+message.manager.action.aproaming-failed = Unable to update setting for WiFi Recovery Mode: {0}
+message.manager.action.resetsta-info = The WiFi STA/AP Cache will be cleared and the device reconnects to the strongest Access Point.
+message.manager.action.resetsta-confirm = Device is reconnecting to the strongest WiFi Access Point.
+message.manager.action.resetsta-failed = Unable to clear STA/AP list and reconnect to WiFi: {0}
+message.manager.action.debug-enable = Device Debug will be enabled. Use this feature only if requested by Allterco Support. 
+message.manager.action.debug-disable = Device Debug will be disabled.
+message.manager.action.debug-confirm = Device Debug was {0}.
+message.manager.action.getdebug-failed = Unable to get Debug Log: {0}
+
+message.manager.coiot.multicast-not-supported = Device doesn't support CoIoT Multicast updates.<br/>Make sure to setup openHAB as CoIoT Peer Address ({0}).
+message.manager.coiot.mode-not-suppored = Device doesn't support request CoIoT Mode ({0}), check product documentation.
+message.manager.coiot.current-peer = CoIoT Peer Address is currently set to {0}.
+message.manager.coiot.new-peer = CoIoT mode/address will be set to {0}.
+message.manager.coiot.mode-mcast = The device starts sending CoIoT updates using IP Multicast.<br/>Please make sure that your network setup supports Multicast routing when devices are on different IP subnets.
+message.manager.coiot.mode-peer = The device will no longer send IP Multicast CoIoT updates to the network, just to the openHAB host.
+
+message.manager.fwupdate.initiated = Firmware update initiated, device returned status {0}
+message.manager.fwupdate.confirm = Do not power-off or restart device while updating the firmware!
+message.manager.fwupdate.info = Update firmware (deviceType={0}, version={1}, URL={2})
+message.manager.fwupdate.failed = Firmware updated failed: {0}
+message.manager.fwupdate.notfound = Unable to find firmware for device type {0}, version={1} (URL={2})
+message.manager.fwupdate.nofile = No firmware files found for device type {0}
+message.manager.fwupdate.success = Firmware successfully loaded - size={0}, ETag={1}, last modified={2}
index 1275f1ba387c812c3b3fb75c74c53173e7829875..70d0209926cb42b7ec958bf87c9d80a9343780c0 100644 (file)
@@ -28,8 +28,8 @@ config-status.error.missing-userid = Keine Benutzerkennung in der Thing Konfigur
 
 # General messages
 message.versioncheck.failed = Firmware-Version konnte nicht geprüft werden: {0}
-message.versioncheck.beta = Es wurde eine Betaversion erkannt: {0}/{1} ({2}), bitte sicherstellen, dass diese neuer ist als Version {3} (Release Build).
-message.versioncheck.tooold = ACHTUNG: Eine alte Firmware wurde erkannt: {0}/{1} ({2}), minimal erforderlich {3}.
+message.versioncheck.beta = Es wurde eine Betaversion erkannt: {0}/{1}.
+message.versioncheck.tooold = ACHTUNG: Eine alte Firmware wurde erkannt: {0}/{1}, minimal erforderlich {2}.
 message.versioncheck.update = INFO: Eine neue Firmwareversion ist verfügbar, aktuell: {0}, neu: {1}
 message.versioncheck.autocoiot = INFO: Die Firmware unterstützt die Anforderung, Auto-CoIoT wurde aktiviert.
 message.init.noipaddress = Es konnte keine lokale IP-Adresse ermittelt werden. Bitte sicherstellen, dass IPv4 aktiviert ist und das richtige Interface in der openHAB Netzwerk-Konfiguration ausgewählt ist.
diff --git a/bundles/org.openhab.binding.shelly/src/main/resources/images/cloud_connected.png b/bundles/org.openhab.binding.shelly/src/main/resources/images/cloud_connected.png
new file mode 100644 (file)
index 0000000..0204906
Binary files /dev/null and b/bundles/org.openhab.binding.shelly/src/main/resources/images/cloud_connected.png differ
diff --git a/bundles/org.openhab.binding.shelly/src/main/resources/images/cloud_disabled.png b/bundles/org.openhab.binding.shelly/src/main/resources/images/cloud_disabled.png
new file mode 100644 (file)
index 0000000..591a189
Binary files /dev/null and b/bundles/org.openhab.binding.shelly/src/main/resources/images/cloud_disabled.png differ
diff --git a/bundles/org.openhab.binding.shelly/src/main/resources/images/cloud_enabled.png b/bundles/org.openhab.binding.shelly/src/main/resources/images/cloud_enabled.png
new file mode 100644 (file)
index 0000000..0c31a37
Binary files /dev/null and b/bundles/org.openhab.binding.shelly/src/main/resources/images/cloud_enabled.png differ
diff --git a/bundles/org.openhab.binding.shelly/src/main/resources/images/cloud_unknown.png b/bundles/org.openhab.binding.shelly/src/main/resources/images/cloud_unknown.png
new file mode 100644 (file)
index 0000000..98eb950
Binary files /dev/null and b/bundles/org.openhab.binding.shelly/src/main/resources/images/cloud_unknown.png differ
diff --git a/bundles/org.openhab.binding.shelly/src/main/resources/images/mqtt_connected.png b/bundles/org.openhab.binding.shelly/src/main/resources/images/mqtt_connected.png
new file mode 100644 (file)
index 0000000..e5b41ea
Binary files /dev/null and b/bundles/org.openhab.binding.shelly/src/main/resources/images/mqtt_connected.png differ
diff --git a/bundles/org.openhab.binding.shelly/src/main/resources/images/mqtt_disabled.png b/bundles/org.openhab.binding.shelly/src/main/resources/images/mqtt_disabled.png
new file mode 100644 (file)
index 0000000..c81d564
Binary files /dev/null and b/bundles/org.openhab.binding.shelly/src/main/resources/images/mqtt_disabled.png differ
diff --git a/bundles/org.openhab.binding.shelly/src/main/resources/images/mqtt_enabled.png b/bundles/org.openhab.binding.shelly/src/main/resources/images/mqtt_enabled.png
new file mode 100644 (file)
index 0000000..6f87024
Binary files /dev/null and b/bundles/org.openhab.binding.shelly/src/main/resources/images/mqtt_enabled.png differ
diff --git a/bundles/org.openhab.binding.shelly/src/main/resources/images/mqtt_unknown.png b/bundles/org.openhab.binding.shelly/src/main/resources/images/mqtt_unknown.png
new file mode 100644 (file)
index 0000000..98eb950
Binary files /dev/null and b/bundles/org.openhab.binding.shelly/src/main/resources/images/mqtt_unknown.png differ
diff --git a/bundles/org.openhab.binding.shelly/src/main/resources/images/otacheck.png b/bundles/org.openhab.binding.shelly/src/main/resources/images/otacheck.png
new file mode 100644 (file)
index 0000000..a920c74
Binary files /dev/null and b/bundles/org.openhab.binding.shelly/src/main/resources/images/otacheck.png differ
diff --git a/bundles/org.openhab.binding.shelly/src/main/resources/images/refresh.png b/bundles/org.openhab.binding.shelly/src/main/resources/images/refresh.png
new file mode 100644 (file)
index 0000000..dbc9c41
Binary files /dev/null and b/bundles/org.openhab.binding.shelly/src/main/resources/images/refresh.png differ
diff --git a/bundles/org.openhab.binding.shelly/src/main/resources/images/resetstat.png b/bundles/org.openhab.binding.shelly/src/main/resources/images/resetstat.png
new file mode 100644 (file)
index 0000000..eefd4ca
Binary files /dev/null and b/bundles/org.openhab.binding.shelly/src/main/resources/images/resetstat.png differ
diff --git a/bundles/org.openhab.binding.shelly/src/main/resources/images/status_attention.png b/bundles/org.openhab.binding.shelly/src/main/resources/images/status_attention.png
new file mode 100644 (file)
index 0000000..f2b0ef3
Binary files /dev/null and b/bundles/org.openhab.binding.shelly/src/main/resources/images/status_attention.png differ
diff --git a/bundles/org.openhab.binding.shelly/src/main/resources/images/status_config.png b/bundles/org.openhab.binding.shelly/src/main/resources/images/status_config.png
new file mode 100644 (file)
index 0000000..00f603a
Binary files /dev/null and b/bundles/org.openhab.binding.shelly/src/main/resources/images/status_config.png differ
diff --git a/bundles/org.openhab.binding.shelly/src/main/resources/images/status_initializing.png b/bundles/org.openhab.binding.shelly/src/main/resources/images/status_initializing.png
new file mode 100644 (file)
index 0000000..f9f866e
Binary files /dev/null and b/bundles/org.openhab.binding.shelly/src/main/resources/images/status_initializing.png differ
diff --git a/bundles/org.openhab.binding.shelly/src/main/resources/images/status_offline.png b/bundles/org.openhab.binding.shelly/src/main/resources/images/status_offline.png
new file mode 100644 (file)
index 0000000..b4cf7fe
Binary files /dev/null and b/bundles/org.openhab.binding.shelly/src/main/resources/images/status_offline.png differ
diff --git a/bundles/org.openhab.binding.shelly/src/main/resources/images/status_online.png b/bundles/org.openhab.binding.shelly/src/main/resources/images/status_online.png
new file mode 100644 (file)
index 0000000..0d415e0
Binary files /dev/null and b/bundles/org.openhab.binding.shelly/src/main/resources/images/status_online.png differ
diff --git a/bundles/org.openhab.binding.shelly/src/main/resources/images/status_uninitialized.png b/bundles/org.openhab.binding.shelly/src/main/resources/images/status_uninitialized.png
new file mode 100644 (file)
index 0000000..2109688
Binary files /dev/null and b/bundles/org.openhab.binding.shelly/src/main/resources/images/status_uninitialized.png differ
diff --git a/bundles/org.openhab.binding.shelly/src/main/resources/images/status_unknown.png b/bundles/org.openhab.binding.shelly/src/main/resources/images/status_unknown.png
new file mode 100644 (file)
index 0000000..067cb01
Binary files /dev/null and b/bundles/org.openhab.binding.shelly/src/main/resources/images/status_unknown.png differ
diff --git a/bundles/org.openhab.binding.shelly/src/main/resources/images/wifi.png b/bundles/org.openhab.binding.shelly/src/main/resources/images/wifi.png
new file mode 100644 (file)
index 0000000..98eb950
Binary files /dev/null and b/bundles/org.openhab.binding.shelly/src/main/resources/images/wifi.png differ
diff --git a/bundles/org.openhab.binding.shelly/src/main/resources/images/wifi0.png b/bundles/org.openhab.binding.shelly/src/main/resources/images/wifi0.png
new file mode 100644 (file)
index 0000000..c407bee
Binary files /dev/null and b/bundles/org.openhab.binding.shelly/src/main/resources/images/wifi0.png differ
diff --git a/bundles/org.openhab.binding.shelly/src/main/resources/images/wifi1.png b/bundles/org.openhab.binding.shelly/src/main/resources/images/wifi1.png
new file mode 100644 (file)
index 0000000..bd6ce2f
Binary files /dev/null and b/bundles/org.openhab.binding.shelly/src/main/resources/images/wifi1.png differ
diff --git a/bundles/org.openhab.binding.shelly/src/main/resources/images/wifi2.png b/bundles/org.openhab.binding.shelly/src/main/resources/images/wifi2.png
new file mode 100644 (file)
index 0000000..54e316a
Binary files /dev/null and b/bundles/org.openhab.binding.shelly/src/main/resources/images/wifi2.png differ
diff --git a/bundles/org.openhab.binding.shelly/src/main/resources/images/wifi3.png b/bundles/org.openhab.binding.shelly/src/main/resources/images/wifi3.png
new file mode 100644 (file)
index 0000000..3fecef7
Binary files /dev/null and b/bundles/org.openhab.binding.shelly/src/main/resources/images/wifi3.png differ
diff --git a/bundles/org.openhab.binding.shelly/src/main/resources/images/wifi4.png b/bundles/org.openhab.binding.shelly/src/main/resources/images/wifi4.png
new file mode 100644 (file)
index 0000000..027c6a6
Binary files /dev/null and b/bundles/org.openhab.binding.shelly/src/main/resources/images/wifi4.png differ
diff --git a/bundles/org.openhab.binding.shelly/src/main/resources/sniplets/action.html b/bundles/org.openhab.binding.shelly/src/main/resources/sniplets/action.html
new file mode 100644 (file)
index 0000000..1946c45
--- /dev/null
@@ -0,0 +1,30 @@
+    <div class="page">
+        <p class="caption">Device Action: <td>${action}</p>
+        <hr/>
+        <p/>
+        <table>
+            <tr><td>Device</td><td>${thingLabel} (${serviceName})</td></tr>
+            <tr><td>Device IP</td><td>${deviceIp} (WiFi ${wifiNetwork})</td></tr>
+            <tr><td><p/></td></tr>
+        </table>
+    
+        <p class="top-distance20">
+            <span class="message">${message}</span>
+            <span id="actionMessage" class="message">Please wait ${refreshTimer}s while status is updated.</span>
+        </p>
+        
+        <p style="padding-left: 6px;">
+           <button id="actionButton" class="button"       onclick="location = '${actionUrl}'">${actionButtonLabel}</button> &nbsp;&nbsp;
+        </p>
+        
+        <script type="text/JavaScript">
+            var refreshTimer = ${refreshTimer};
+            if (refreshTimer > 0) { 
+                setTimeout("location.href = '${uri}/overview';",(refreshTimer+1)*1000);
+                document.getElementById("actionButton").style.visibility="hidden";
+            } else {
+                document.getElementById("actionMessage").style.visibility="hidden";
+            }
+        </script>
+    </div>
+    <p/>
diff --git a/bundles/org.openhab.binding.shelly/src/main/resources/sniplets/footer.html b/bundles/org.openhab.binding.shelly/src/main/resources/sniplets/footer.html
new file mode 100644 (file)
index 0000000..ae75417
--- /dev/null
@@ -0,0 +1,13 @@
+    </tbody></table>
+    <p/>
+    <hr/>
+    <div class="navFooter">
+        <a class="navFooter" href="${uri}">Device Overview</a> | <a href="/">openHAB Home</a>
+        ${numberDevices}
+    </div>
+    
+    ${cssFooter}
+    <p/>
+
+</body>
+</html>
diff --git a/bundles/org.openhab.binding.shelly/src/main/resources/sniplets/forward.script b/bundles/org.openhab.binding.shelly/src/main/resources/sniplets/forward.script
new file mode 100644 (file)
index 0000000..3c2e4b3
--- /dev/null
@@ -0,0 +1,3 @@
+            <script type="text/JavaScript">
+                setTimeout("location.href = '${forwardLink}';",${forwardTimer});
+            </script>
diff --git a/bundles/org.openhab.binding.shelly/src/main/resources/sniplets/fw_update1.html b/bundles/org.openhab.binding.shelly/src/main/resources/sniplets/fw_update1.html
new file mode 100644 (file)
index 0000000..8a1af6c
--- /dev/null
@@ -0,0 +1,50 @@
+<div class="page">
+    <p class="caption">Firmware update</p>
+    <hr/>
+    <p/>
+       <table style="padding-right: 6px;">
+               <tr><td>Device</td><td>${serviceName}</td></tr>
+               <tr><td>Device Type</td><td>${modelId}</td></tr>
+               <tr><td>Device Mode</td><td>${deviceMode}</td></tr>
+               <tr><td>Device IP</td><td><a href="http://${deviceIp}" title="${deviceName}"target="_blank">${deviceIp}</a></td></tr>
+               <tr><td style="padding-left: 10px">Device Hardware Rev</td><td>${deviceHwRev}</td></tr>
+               <tr><td>WiFi</td><td>${wifiNetwork}</td></tr>
+               <tr><td><p/></td></tr>
+               <tr><td>Current firmware</td><td>${firmwareVersion}</td></tr>
+               <tr><td>Requested version</td><td>${version}</td></tr>
+       </table>
+       <p/>
+       
+    <form style="padding: 6px 6px;">
+        <p>Please select connection type:</p>
+        <input type="radio" name="connection" id="internet" value="internet"/>
+            <label for="internet">Device downloads firmware directly from Internet</label><br/>
+        <input type="radio" name="connection" id="local" value="local"/>
+            <label for="local">Use openHAB as proxy (device doesn't requires Internet access)</label><br/>
+        <input type="radio" name="connection" id="custom" value="custom"/>
+            <label for="custom">Use custom URL</label>
+            <div style="padding-left: 38px;">
+                <input type="text" name = "url" id="url" value="http://" style=" padding: 3px 3px;/>
+                    <label for="url">&nbsp;</label>
+            </div>
+
+           <input type="hidden" id="uid" name="uid" value="${uid}">
+           <input type="hidden" id="version" name="version" value="${version}">
+           <input type="hidden" id="update" name="update" value="yes">
+           
+           <p class="top-distance20">
+                   <button class="button">Perform Update</button>&nbsp;&nbsp;
+                   <button type="button" class="buttonCancel" onclick="location = '${uri}/overview'">Abort</button>
+        </p>
+        <p/>
+    </form>
+    <script type="text/JavaScript">
+        const connection = "${connection}";
+        document.getElementById(connection.valueOf() === "" ? "internet" : "${connection}").checked = true;                        
+    </script>
+    <p/>
+
+       <p class="info">${message}</p>
+</div>
+<p/>
+       
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 (file)
index 0000000..6d0b63e
--- /dev/null
@@ -0,0 +1,17 @@
+    <div class="page">
+           <p class="caption">Firmware Update</p>
+           <hr/><p/>
+           <p class="message">Updating device ${deviceName} (${uid}) with version ${version}, connection type=${connection}</p>
+        <p class="message" sytle>Update url: ${updateUrl}</p>
+        
+           <span class="top-distance20">
+              <p class="info">Wait 1-2 minutes, then check device UI at <a href="http://${deviceIp}" title="${thingName}" target="_blank">${deviceIp}</a>, section Firmware.</p>
+              <p class="warning">Do not power-off or restart device while updating the firmware!</p>
+           </span>
+
+        <p>
+              <button class="button" onclick="location = '${uri}/overview'">Ok</button>
+           </p>
+    </div>
+    <p/>
+            
\ 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 (file)
index 0000000..4205bee
--- /dev/null
@@ -0,0 +1,43 @@
+<!DOCTYPE html>
+<html lang="en" ng-app="Shelly Manager">
+
+    <title>Shelly Manager</title>
+    <meta charset="utf-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
+    ${metaTag}
+    
+    <style type=text/css>
+    body {
+        height:100%, width:100%; padding: 0; margin: 0;
+        font-family: 'Roboto', Sans-Serif; color: white;
+        background-color: #262D2F;
+    }
+    .page { padding: 6px 6px; }
+    .navFooter { font-size:12px; padding-top: 2px; }
+    .footerDevices { float: right; padding-right: 3px; color:#00cc00; }
+    .top-distance20 { padding-top: 20px; paddin-left: 6px; }
+
+   .caption       { color:#2886c7; font-size:18px; font-weight: bold; font-style: italic; }
+   .caption2      { color:#2886c7; font-size:14px; font-weight: bold; font-style: italic; }
+   .message       { padding-left: 6px; }
+   .blue, .info   { color:#2886c7; padding-left: 6px; }
+   .red, .warning { color:#cb4a4a; padding-left: 6px; }
+   .green         { color:#00cc00; }
+
+    a:link    { color: #3399ff; background-color: transparent; text-decoration: none; }
+    a:visited { color: #00cc00; background-color: transparent; text-decoration: none; }
+    a:hover   { color: #00f700; background-color: transparent; text-decoration: underline; }
+    a:active  { color: yellow; background-color: transparent; text-decoration: underline; }
+
+    .button { background-color: #FF6600; color: white; border: 0; hight: 14px; min-width: 50px; font-size:14px; padding: 6px 6px; }
+    .buttonCancel { color: #2886c7; border: 1px; border-color: white; hight: 14px; min-width: 50px; font-size:14px; padding: 6px 6px; }
+       input[type='radio'], label{ vertical-align: baseline; padding: 4px; margin: 6px; }
+       input[type='text'] { vertical-align: baseline; color: white; font-size: 12px; background-color: #262D2F; height: 16px; border: 1px solid; border-color: #2886c7;}
+       .select-hr { border: 1px; border-style: solid; padding-top: 2px; color: white; border-color: white; text-color: white; }
+    
+    </style>
+        
+    ${cssHeader}
+</head>
+
+<body>
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 (file)
index 0000000..f487a4f
--- /dev/null
@@ -0,0 +1,106 @@
+        <tr >
+            <td>
+                <div class="tooltip">
+                    <div>
+                        <img src="${uri}/images/status_${iconStatus}.png" class="statusIcon"/>                
+                    </div>
+                    <div class="tooltiptext">
+                        <table style="text-align:left; ">
+                            <tr><td colspan = "2" class="caption2">${thingName}</td></tr>
+                            <tr><td>Status</td><td>${thingStatus} ${thingStatusDetail}</td></tr>
+                            <tr><td>CoIoT Status</td><td>${coiotStatus}</td></tr>
+                            <tr><td>CoIoT Destination</td><td>${coiotDestination}</td></tr>
+                            <tr><td>Cloud Status</td><td>${cloudStatus}</td></tr>
+                            <tr><td>MQTT Status</td><td>${mqttStatus}</td></tr>
+                            <tr><td>Actions skipped</td><td>${actionsSkipped}</td></tr>
+                            <tr><td>Max Internal Temp</td><td>${maxInternalTemp}</td></tr>
+                            <tr><td>&nbsp;<br/></td></tr>
+                            ${deviceStatus}
+                        </table>
+                    </div>
+                </div>
+            </td>
+            <td>
+                <div class="tooltip" style="border: 1px; border-color: white;">
+                    <span style="white-space: nowrap;">${displayName}</span>
+                    <div class="tooltiptext">
+                        <table style="text-align:left; ">
+                            <tr><td colspan = "2" class="caption2">${thingName}</td></tr>
+                            <tr><td>Shelly Device Name</td><td>${deviceName}</td></tr>
+                            <tr><td>Device Hardware Rev</td><td>${deviceHwRev}</td></tr>
+                            <tr><td>Device Type</td><td>${modelId}</td></tr>
+                            <tr><td>Device Mode</td><td>${deviceMode}</td></tr>
+                            <tr><td>Firmware Version</td><td>${firmwareVersion}</td></tr>
+                            <tr><td>Network Name</td><td>${serviceName}</td></tr>
+                            <tr><td>MAC Address</td><td>${macAddress}</td></tr>
+                            <tr><td>Discoverable</td><td>${discoverable}</td></tr>
+                            <tr><td>WiFi Auto Recovery</td><td>${wifiAutoRecovery}</td></tr>
+                            <tr><td>WiFi AP Roaming</td><td>${apRoamingMode}</td></tr>
+                            <tr><td>WiFi AP Threshold</td><td>${apRoamingThreshold}</td></tr>
+                            <tr><td>Timezone</td><td>${deviceTimezone}</td></tr>
+                            <tr><td>Time Server</td><td>${sntpServer}</td></tr>
+                            <tr><td>Debug Mode</td><td>${debugMode}</td></tr>
+                        </table>
+                    </div>
+                </div>
+            </td>
+            <td>
+                <div title = "Cloud ${cloudStatus}">
+                    <img src="${uri}/images/cloud_${cloudStatus}.png" class="icon"/>
+                </div>
+            </td>
+            <td>
+                <div title = "MQTT ${mqttStatus}">
+                    <img src="${uri}/images/mqtt_${mqttStatus}.png" class="icon"/>
+                </div>
+            </td>
+            <td>
+                <div title="Refresh Device Status">
+                       <a href="${uri}/overview?action=refresh&uid=${uid}">
+                           <img src="${uri}/images/refresh.png" class="icon"/>
+                       </a>
+                </div>
+            </td>
+            <td>
+                <div title="Reset Device Statistic">
+                    <a href="${uri}/action?action=reset_stat&uid=${uid}">
+                        <img src="${uri}/images/resetstat.png" class="icon"/>
+                    </a>
+                </div>
+            </td>
+            <td>
+                <div title="Check for new firmware">
+                    <a href="${uri}/action?action=otacheck&uid=${uid}">
+                        <img src="${uri}/images/otacheck.png" class="icon"/>
+                    </a>
+                </div>
+            </td>
+            <td><a href="http://${deviceIp}" title="${displayName}" target="_blank">${deviceIp}</a></td>
+            <td>${wifiNetwork}</td>
+            <td >
+                <div title = "Cloud ${cloudStatus}">
+                    <img src="${uri}/images/wifi${wifiSignal}.png" class="icon" alt="Signal quality: ${wifiSignal} (4=best..1=very weak)"/>
+                </div>
+            </td>
+            <td align="right"  nowrap>&nbsp;${wifiSignalRssi}</td>
+            <td align="center" nowrap>${batteryLevel}</td>
+            <td>${heartBeat}</td>
+            <td>${actionList}</td>
+            <td nowrap>${firmwareVersion}</td>
+            <td align="center">${updateAvailable}</td>
+            <td>${firmwareSelection}</td>
+            <td align="right" nowrap>${uptime}</td>
+            <td align="center" nowrap title="Max Internal Device Temp so far: ${maxInternalTemp}">${internalTemp}</td>
+            <td align="right" nowrap>${devUpdatePeriod} s</td>
+            <td align="right">${remainingWatchdog} s</td>
+            <td align="right">${alarmCount}</td>
+            <td nowrap>
+                <a href="${uri}/action?action=reset_stat&uid=${uid}" title="Clear alarm">${lastAlarm}</a>
+            </td>
+            <td>${lastAlarmTs}</td>
+            <td align="right">${deviceRestarts}</td>
+            <td align="right">${timeoutErrors}</td>
+            <td align="right">${timeoutsRecovered}</td>
+            <td align="right" title="CoIoT Status: ${coiotStatus}">${coiotMessages}</td>
+            <td align="right">${coiotErrors}</td>
+        </tr>
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 (file)
index 0000000..b48598c
--- /dev/null
@@ -0,0 +1,33 @@
+   <script>
+        var tooltips = document.querySelectorAll(".tooltip");
+        tooltips.forEach(function(tooltip, index)
+        {
+            tooltip.addEventListener("mouseover", position_tooltip); // On hover, launch the function below
+        })
+
+        function position_tooltip(){
+          // Get .tooltiptext sibling
+          var tooltip = this.parentNode.querySelector(".tooltiptext");
+          
+          // Get calculated tooltip coordinates and size
+          var tooltip_rect = this.getBoundingClientRect();
+
+          var tipX = tooltip_rect.width + 5; // 5px on the right of the tooltip
+          var tipY = -40;                     // 40px on the top of the tooltip
+          // Position tooltip
+          tooltip.style.top = tipY + 'px';
+          tooltip.style.left = tipX + 'px';
+
+          // Get calculated tooltip coordinates and size
+          var tooltip_rect = tooltip.getBoundingClientRect();
+          // Corrections if out of window
+          if ((tooltip_rect.x + tooltip_rect.width) > window.innerWidth) // Out on the right
+            tipX = -tooltip_rect.width - 5;  // Simulate a "right: tipX" position
+          if (tooltip_rect.y < 0)            // Out on the top
+            tipY = tipY - tooltip_rect.y;    // Align on the top
+
+          // Apply corrected position
+          tooltip.style.top = tipY + 'px';
+          tooltip.style.left = tipX + 'px';
+        }
+    </script>
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 (file)
index 0000000..e7716f7
--- /dev/null
@@ -0,0 +1,37 @@
+    <style> 
+        .navigation table { width:100%; vertical-align: middle; text-align:right; float:right; font-size:12px; border: 0; }
+        .navigation tr { border:0; }
+        .navigation img { vertical-align: middle; width: 14px; height: 14px; }
+        .navigation select, option { background-color: #555; color:white; box-sizing: border-box;  }
+        .navigation a:hover { color: transparent; background-color: transparent; text-decoration: none;}
+    
+        .devTable { border-collapse:collapse;font-size:12px; color:white; }
+        .devTable th { background-color:#0B398C; }
+        .devTable tr:nth-child(even) { }
+        .devTable tr:nth-child(odd)  { background: #555; border: 0; }
+        .devTable td, .devTable th { padding:3px 3px; white-space: wrap; border: 0; }
+        .devTable select, option { background-color: #555; color:white; box-sizing: border-box; width: 120px; }
+        
+        .navRefIcon { margin: 0 auto;  vertical-align: middle; width: 20px; height: 20px; border: 0;}
+        .statusIcon { margin: 0 auto;  vertical-align: middle; width: 12px; height: 12px; border: 0;}
+        .icon       { margin: 0 auto;  vertical-align: middle; width: 14px; height: 16px; border: 0;}
+
+        .tooltip { position: relative; display: inline-block; }
+        .tooltiptext table { padding: 7px 7px; background: #555; border: 0; white-space: nowrap;}
+        .tooltip:hover .tooltiptext { visibility: visible; opacity: 1; }
+
+        .tooltip .tooltiptext {
+          visibility: hidden;
+          color: #fff;
+          text-align: left;
+          border-radius: 5px;
+          padding: 5px 5px;
+          position: absolute;
+          z-index: 1;
+          bottom: 125%;
+          left: 50%;
+          margin-left: -50%;
+          opacity: 0;
+          transition: opacity 0.3s;
+        }
+    </style>
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 (file)
index 0000000..f6ced5f
--- /dev/null
@@ -0,0 +1,60 @@
+    <div class="navigation">
+        <table><tr><td>
+            Device Filter&nbsp;
+            <select name="thingFilter" id="thingFilter" onchange="location = '${uri}/overview?filter='+this.options[this.selectedIndex].value;">
+                <option value="" selected disabled>select</option>
+                <option value="*">All</option>
+                <option value="online">Online only</option>
+                <option value="inactive">Inactive only</option>
+                <option value="attention">Needs Attention</option>
+                <option value="update">Update available</option>
+                <option value="unprotected">Unprotected</option>
+            </select>
+            <a href="${uri}/overview?action=refresh">
+                <img src="${uri}/images/refresh.png" class="navRefIcon" title="Refresh Status for all Devices"/>&nbsp;&nbsp;
+            </a>
+            <a href="${uri}/overview?action=reset_stat">
+                <img src="${uri}/images/resetstat.png" class="navRefIcon" title="Reset Statistics for all Devices"/>&nbsp;&nbsp;
+            </a>
+            <a href="${uri}/overview?action=otacheck">
+                <img src="${uri}/images/otacheck.png" class="navRefIcon" title="Trigger Firmware Check for all Devices"/>&nbsp;&nbsp;
+            </a>
+        </td></tr></table>
+    </div>
+    
+    <div class="overview">
+           &nbsp;<hr/>
+           &nbsp;<br/>
+           
+           <table class="devTable">
+               <tbody>
+               <tr>
+                   <th>S</th>
+                   <th align="left">Name</th>
+                <th>&nbsp;</th>
+                <th>&nbsp;</th>
+                <th>&nbsp;</th>
+                <th>&nbsp;</th>
+                <th>&nbsp;</th>
+                   <th align="left">Device IP</th>
+                   <th align="left">WiFi Network</th>
+                   <th colspan = "2">WiFi Signal</th>
+                   <th>Battery Level</th>
+                   <th align="left">Heartbeat</th>
+                   <th>Actions</th>
+                   <th align="left">Firmware</th>
+                   <th>Update avail</th>
+                   <th align="left">Versions</th>
+                   <th>Uptime</th>
+                   <th>Internal Temp</th>
+                   <th>Update Period</th>
+                   <th>Remaining Watchdog</th>
+                   <th>Events</th>
+                   <th>Last Event</th>
+                   <th>Event Time</th>
+                   <th>Device Restarts</th>
+                   <th>Timeout Errors</th>
+                   <th>Timeouts Recovered</th>
+                   <th>CoIOT Messages</th>
+                   <th>CoIOT Errors</th>
+               </tr>