[shelly] New Shelly Manager (more detailed information, status, integrated firmware upgrade) (#10276)
* This PR brings in the Shelly Manager, check doc/ShellyManager.md for additional information. * Restart Device in Manager when CoIoT Mode has changed * Updated pattern to extract version info (thanks @fwolter), CoIoT warning for non-Motion devices fixed; AdvancedUsers.md now refers to Shelly Manager * Modified message when beta is detected (reference to 1.5.7 release build confuses users running 1.10 beta) * Fix for Enable/Disable AP roaming * Handle button events also in detached mode, README updated * Ignore inconsistent version string for initial 1.10 releases * removed display of firmware id (there are various formats and it has no value) Signed-off-by: Markus Michels <markus7017@gmail.com>
|
@ -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.
|
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
|
## Supported Devices
|
||||||
|
|
||||||
| thing-type | Model | Vendor ID |
|
| thing-type | Model | Vendor ID |
|
||||||
|
@ -257,16 +265,20 @@ The following trigger types are sent:
|
||||||
|
|
||||||
|Event Type |Description |
|
|Event Type |Description |
|
||||||
|-------------------|---------------------------------------------------------------------------------------------------------------|
|
|-------------------|---------------------------------------------------------------------------------------------------------------|
|
||||||
|SHORT_PRESSED |The button was pressed once for a short time |
|
|SHORT_PRESSED |The button was pressed once for a short time (lastEvent=S) |
|
||||||
|DOUBLE_PRESSED |The button was pressed twice with short delay |
|
|DOUBLE_PRESSED |The button was pressed twice with short delay (lastEvent=SS) |
|
||||||
|TRIPLE_PRESSED |The button was pressed three times with short delay |
|
|TRIPLE_PRESSED |The button was pressed three times with short delay (lastEvent=SSS) |
|
||||||
|LONG_PRESSED |The button was pressed for a longer time |
|
|LONG_PRESSED |The button was pressed for a longer time (lastEvent=L) |
|
||||||
|SHORT_LONG_PRESSED |A short followed by a long button push |
|
|SHORT_LONG_PRESSED |A short followed by a long button push (lastEvent=SL) |
|
||||||
|LONG_SHORT_PRESSED |A long followed by a short button push |
|
|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.
|
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.
|
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
|
### Alarms
|
||||||
|
|
||||||
The binding provides health monitoring functions for the device.
|
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.
|
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 |
|
|Group |Channel |Type |read-only|Description |
|
||||||
|----------|---------------|---------|---------|---------------------------------------------------------------------|
|
|----------|---------------|---------|---------|---------------------------------------------------------------------|
|
||||||
|
|
|
@ -0,0 +1,186 @@
|
||||||
|
# Shelly Manager
|
||||||
|
|
||||||
|
The Shelly Manager is a small extension to the binding, which provides some low level information on the Shelly Devices, but also provides some functions to manage the devices.
|
||||||
|
|
||||||
|
To open the Shelly Manage launch the following URL in your browser
|
||||||
|
- http://<openHAB IP address>:8080/shelly/manager or
|
||||||
|
- http://<openHAB IP address>:8443/shelly/manager
|
||||||
|
|
||||||
|
Maybe you need to change the port matching your setup.
|
||||||
|
|
||||||
|
Shelly Manager makes you various device insights available to get an overview of your Shellys
|
||||||
|
- Get a quick overview that all Shellys operate like expected, statistical data will help to identify issues
|
||||||
|
- Have some basic setting actions integrated, which help to do an easy setup of new Shellys added to openHAB
|
||||||
|
- Make firmware updates way easier - filter 'Update available' + integrated 2-click update
|
||||||
|
- Provide a firmware download proxy, which allows to separate your Shellys from the Internet (improved device security)
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Once the Shelly Manager is opened an overview of all Shelly devices added as a Thing are displayed.
|
||||||
|
Things which are not discovered or still site in the Inbox will not be displayed.
|
||||||
|
|
||||||
|
![](images/manager/overview.png)
|
||||||
|
|
||||||
|
You'll see a bunch of technical details, which are not available as channels or in the Thing properties.
|
||||||
|
This includes information on the device communication stability.
|
||||||
|
The statistic gives you a good overview if device communication is stable or a relevant number of timeouts need to be recovered.
|
||||||
|
In this case you should verify the WiFi coverage or other options to improve stability.
|
||||||
|
|
||||||
|
The following information is available
|
||||||
|
|Column |Description |
|
||||||
|
|--------------------|---------------------------------------------------------------------------------|
|
||||||
|
|S |Thing Status - hover over the icon to see more details |
|
||||||
|
|Name |Device name - hover over the name to get more details |
|
||||||
|
|Cloud Status Icon |Indicates the status of the Shelly Cloud feature: disabled/enabled/connected |
|
||||||
|
|MQQT Status Icon |Indicates the staus of the MQTT featured disabled/enabled/connected |
|
||||||
|
|Refresh button |Trigger a status refresh in background, maybe you need to click more than once |
|
||||||
|
|Device IP |Assigned IP address, click to open the device’s Web UI in a separate browser tab |
|
||||||
|
|WiFi Network |SSID of the connected WiFi network |
|
||||||
|
|WiFi Signal |WiFi signal strength, 0=none, 4=very good |
|
||||||
|
|Battery Level |Remaining capacity of the battery |
|
||||||
|
|Heartbeat |Last time a response or an event was received from the device |
|
||||||
|
|Actions |Drop down with some actions, see below |
|
||||||
|
|Firmware |Current firmware release |
|
||||||
|
|Update avail |yes indicates that a firmware update is available |
|
||||||
|
|Versions |List available firmware versions: prod, beta or archived |
|
||||||
|
|Uptime |Number of seconds since last device restart |
|
||||||
|
|Internal Temp |Device internal temperature. Max is depending on device type. |
|
||||||
|
|Update Period |Timeout for device refresh |
|
||||||
|
|Remaining Watchdog |Shows number of seconds until device will go offline if no update is received |
|
||||||
|
|Events |Increases on every event triggered by the device or the binding |
|
||||||
|
|Last Event |Type of last event or alarm (refer README.md for details) |
|
||||||
|
|Event Time |When was last event received |
|
||||||
|
|Device Restarts |Number of detected restarts. This is ok on firmware updates, otherwise indicates a crash |
|
||||||
|
|Timeout Errors |Number of API timeouts, could be an indication for an unstable connection |
|
||||||
|
|Timeouts recovered |The binding does retries and timeouts and counts successful recoveries |
|
||||||
|
|CoIoT Messages |Number of received CoIoT messages, must be >= 2 to indicate CoIoT working |
|
||||||
|
|CoIoT Errors |Number of CoIoT messages, which can't be processed. >0 indicates firmware issues |
|
||||||
|
|
||||||
|
The column S and Name display more information when hovering with the mouse over the entries.
|
||||||
|
|
||||||
|
![](images/manager/overview_devstatus.png)
|
||||||
|
![](images/manager/overview_devsettings.png)
|
||||||
|
|
||||||
|
### Device Filters
|
||||||
|
|Filter |Description |
|
||||||
|
|--------------------|---------------------------------------------------------------------------------|
|
||||||
|
|All |Clear filter / display all devices |
|
||||||
|
|Online only |Filter on devices with Thing Status = ONLINE |
|
||||||
|
|Inactive only |Filter on devices, which are not initialized for in Thing Status = OFFLINE |
|
||||||
|
|Needs Attention |Filter on devices, which need attention (setup/connectivity issues), see below |
|
||||||
|
|Update available |Filter on devices having a new firmware version available |
|
||||||
|
|Unprotected |Filter on devices, which are currently not password protected |
|
||||||
|
|
||||||
|
Beside the Device Filter box you see a refresh button.
|
||||||
|
At the bottom right you see number of displayed devices vs. number of total devices.
|
||||||
|
A click triggers a background status update for all devices rather only the selected one when clicking of the refresh button in the device lines.
|
||||||
|
|
||||||
|
Filter 'Needs Attention':
|
||||||
|
This is a dynamic filter, which helps to identify devices having some kind of setup / connectivity or operation issues.
|
||||||
|
The binding checks the following conditions
|
||||||
|
- Thing status != ONLINE: Use the 'Inactive Only' filter to find those devices, check openhab.log
|
||||||
|
- WIFISIGNAL: WiFi signal strength < 2 - this usually leads into connectivity problems, check positioning of portable devices or antenna direction.
|
||||||
|
- LOWBATTERY: The remaining battery is < 20% (configuration in Thing Configuration), consider to replace the battery
|
||||||
|
Watch out for bigger number of timeout errors.
|
||||||
|
- Device RESTARTED: Indicates a firmware problem / crash if this happens without a device reboot or firmware update (timestamp is included)
|
||||||
|
- OVERTEMP / OVERLOAD / LOADERROR: There are problems with the physical installation of the device, check specifications, wiring, housing!
|
||||||
|
- SENSORERROR: A sensor error / malfunction was detected, check product documentation
|
||||||
|
- NO_COIOT_DISCOVERY: The CoIoT discovery has not been completed, check IP network configuration, re-discover the device
|
||||||
|
- NO_COIOT_MULTICAST: The CoIoT discovery could be completed, but the device is not receiving CoIoT status updates.
|
||||||
|
You might try to switch to CoIoT Peer mode, in this case the device doesn't use IP Multicast and sends updates directly to the openHAB host.
|
||||||
|
|
||||||
|
The result is shown in the Device Status tooltip.
|
||||||
|
|
||||||
|
### Device settings & status
|
||||||
|
|
||||||
|
When hovering with the mouse over the status icon or the device name you'll get additional information settings and status.
|
||||||
|
|
||||||
|
### Device Status
|
||||||
|
|
||||||
|
|Status |Description |
|
||||||
|
|--------------------|---------------------------------------------------------------------------------|
|
||||||
|
|Status |Thing status, sub-status and description as you know it from openHAB |
|
||||||
|
|CoIoT Status |CoIoT status: enabled or disabled |
|
||||||
|
|CoIoT Destination |CoIoT Peer address (ip address:port) or Multicast |
|
||||||
|
|Cloud Status |Status of the Shelly Cloud connection: disabled, enabled, connected |
|
||||||
|
|MQTT Status |MQTT Status: disabled, enabled, connected |
|
||||||
|
|Actions skipped |Number of actions skipped by the device, usually 0 |
|
||||||
|
|Max Internal Temp |Maximum internal temperature, check device specification for valid range |
|
||||||
|
|
||||||
|
### Device Settings
|
||||||
|
|
||||||
|
|Setting |Description |
|
||||||
|
|--------------------|---------------------------------------------------------------------------------|
|
||||||
|
|Shelly Device Name |Device name according to device settings |
|
||||||
|
|Device Hardware Rev |Hardware revision of the device |
|
||||||
|
|Device Type |Device Type ID |
|
||||||
|
|Device Mode |Selected mode for dual mode devices (relay/roller or white/color) |
|
||||||
|
|Firmware Version |Current firmware version |
|
||||||
|
|Network Name |Network name of the device used for mDNS |
|
||||||
|
|MAC Address |Unique hardware/network address of the device |
|
||||||
|
|Discoverable |true: the device can be discovered using mDNS, false: device is hidden |
|
||||||
|
|WiFi Auto Recovery |enabled: the device will automatically reboot when WiFi connect fails |
|
||||||
|
|Timezone |Configured device zone (see device settings) |
|
||||||
|
|Time server |Configured time server (use device UI to change) |
|
||||||
|
|
||||||
|
### Actions
|
||||||
|
|
||||||
|
The Shelly Manager provides the following actions when the Thing is ONLINE.
|
||||||
|
They are available in the dropdown list in column Actions.
|
||||||
|
|
||||||
|
|Action |Description |
|
||||||
|
|---------------------|---------------------------------------------------------------------------------|
|
||||||
|
|Reset Statistics |Resets device statistic and clear the last alarm |
|
||||||
|
|Restart |Restart the device and reconnect to WiFi |
|
||||||
|
|Protect |Use binding's default credentials to protect device access with user and password|
|
||||||
|
|Set CoIoT Peer |Disable CoIoT Multicast and set openHAB system as receiver for CoIoT updates |
|
||||||
|
|Set CoIoT Multicast |Disable CoIoT Multicast and set openHAB system as receiver for CoIoT updates |
|
||||||
|
|Enable Cloud |Enable the Shelly Cloud connectivity |
|
||||||
|
|Disable Cloud |Disable the Shelly Cloud connectivity (takes about 15sec to become active) |
|
||||||
|
|Reconnect WiFi |Sensor devices only: Clears the STA/AP list and reconnects to strongest AP |
|
||||||
|
|Enable WiFi Roaming |The device will connect to the strongest AP when roadming is enabled |
|
||||||
|
|Disable WiFi Roaming |Disable Access Point Roaming, device will periodically search for better APs |
|
||||||
|
|Enable WiFi Recovery |Enables auto-restart if device detects persistent WiFi connectivity issues |
|
||||||
|
|Disable WiFi Recovery|Disables device auto-restart ion persistent WiFi connectivity issues |
|
||||||
|
|Factory Reset |Performs a **factory reset**; Attention: The device will lose its configuration |
|
||||||
|
|Enable Device Debug |Enables on-device debug log - activate only when requested by Allterco support |
|
||||||
|
|Get Debug Log |Retrieve and display device debug output |
|
||||||
|
|Get Debug Log1 |Retrieve and display 2nd device debug output |
|
||||||
|
|Factory Reset |Performs **firmware reset**; Attention: The device will lose its configuration |
|
||||||
|
|
||||||
|
Note: Various actions available only for certain devices or when using a minimum firmware version.
|
||||||
|
|
||||||
|
![](images/manager/overview_actions.png)
|
||||||
|
|
||||||
|
## Firmware Update
|
||||||
|
|
||||||
|
The Shelly Manager simplifies the firmware update.
|
||||||
|
You could select between different versions using the drop down list on the overview page.
|
||||||
|
|
||||||
|
Shelly Manager integrates different sources
|
||||||
|
- Allterco official releases: production and beta release (like in the device UI)
|
||||||
|
- Older firmware release from the firmware archive - this is a community service
|
||||||
|
- You could specify any custom URL providing the firmware image (e.g. a local web server), which is accessible for the device using http
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|-|-|
|
||||||
|
|![](images/manager/overview_versions.png)|All firmware releases are combined to the selection list.<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.
|
|
@ -194,7 +194,6 @@ public class ShellyBindingConstants {
|
||||||
public static final String PROPERTY_STATS_TIMEOUTS = "statsTimeoutErrors";
|
public static final String PROPERTY_STATS_TIMEOUTS = "statsTimeoutErrors";
|
||||||
public static final String PROPERTY_STATS_TRECOVERED = "statsTimeoutsRecovered";
|
public static final String PROPERTY_STATS_TRECOVERED = "statsTimeoutsRecovered";
|
||||||
public static final String PROPERTY_COIOTAUTO = "coiotAutoEnable";
|
public static final String PROPERTY_COIOTAUTO = "coiotAutoEnable";
|
||||||
public static final String PROPERTY_COIOTREFRESH = "coiotAutoRefresh";
|
|
||||||
|
|
||||||
// Relay
|
// Relay
|
||||||
public static final String CHANNEL_GROUP_RELAY_CONTROL = "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_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_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_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
|
// Alarm types/messages
|
||||||
public static final String ALARM_TYPE_NONE = "NONE";
|
public static final String ALARM_TYPE_NONE = "NONE";
|
||||||
|
|
|
@ -543,7 +543,9 @@ public class ShellyApiJsonDTO {
|
||||||
@SerializedName("wifi_sta1")
|
@SerializedName("wifi_sta1")
|
||||||
public ShellySettingsWiFiNetwork wifiSta1;
|
public ShellySettingsWiFiNetwork wifiSta1;
|
||||||
@SerializedName("wifirecovery_reboot_enabled")
|
@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 ShellySettingsMqtt mqtt; // not used for now
|
||||||
public ShellySettingsSntp sntp; // not used for now
|
public ShellySettingsSntp sntp; // not used for now
|
||||||
|
@ -563,6 +565,7 @@ public class ShellyApiJsonDTO {
|
||||||
public ShellySensorSleepMode sleepMode; // FW 1.6
|
public ShellySensorSleepMode sleepMode; // FW 1.6
|
||||||
@SerializedName("external_power")
|
@SerializedName("external_power")
|
||||||
public Integer externalPower; // H&T FW 1.6, seems to be the same like charger for the Sense
|
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 String timezone;
|
||||||
public Double lat;
|
public Double lat;
|
||||||
|
@ -891,6 +894,15 @@ public class ShellyApiJsonDTO {
|
||||||
public Integer currentPos; // current position 0..100, 100=open
|
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 class ShellySensorSleepMode {
|
||||||
public Integer period;
|
public Integer period;
|
||||||
public String unit;
|
public String unit;
|
||||||
|
|
|
@ -63,6 +63,10 @@ public class ShellyApiResult {
|
||||||
return httpCode == OK_200;
|
return httpCode == OK_200;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isNotFound() {
|
||||||
|
return httpCode == NOT_FOUND_404;
|
||||||
|
}
|
||||||
|
|
||||||
public boolean isHttpAccessUnauthorized() {
|
public boolean isHttpAccessUnauthorized() {
|
||||||
return (httpCode == UNAUTHORIZED_401 || response.contains(SHELLY_APIERR_UNAUTHORIZED));
|
return (httpCode == UNAUTHORIZED_401 || response.contains(SHELLY_APIERR_UNAUTHORIZED));
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.ShellySettingsRelay;
|
||||||
import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsRgbwLight;
|
import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsRgbwLight;
|
||||||
import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsStatus;
|
import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsStatus;
|
||||||
|
import org.openhab.binding.shelly.internal.util.ShellyVersionDTO;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
@ -44,12 +45,13 @@ import com.google.gson.Gson;
|
||||||
@NonNullByDefault
|
@NonNullByDefault
|
||||||
public class ShellyDeviceProfile {
|
public class ShellyDeviceProfile {
|
||||||
private final Logger logger = LoggerFactory.getLogger(ShellyDeviceProfile.class);
|
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 boolean initialized = false; // true when initialized
|
||||||
|
|
||||||
public String thingName = "";
|
public String thingName = "";
|
||||||
public String deviceType = "";
|
public String deviceType = "";
|
||||||
|
public boolean extFeatures = false;
|
||||||
|
|
||||||
public String settingsJson = "";
|
public String settingsJson = "";
|
||||||
public ShellySettingsGlobal settings = new ShellySettingsGlobal();
|
public ShellySettingsGlobal settings = new ShellySettingsGlobal();
|
||||||
|
@ -64,7 +66,6 @@ public class ShellyDeviceProfile {
|
||||||
public String hwRev = "";
|
public String hwRev = "";
|
||||||
public String hwBatchId = "";
|
public String hwBatchId = "";
|
||||||
public String mac = "";
|
public String mac = "";
|
||||||
public String fwId = "";
|
|
||||||
public String fwVersion = "";
|
public String fwVersion = "";
|
||||||
public String fwDate = "";
|
public String fwDate = "";
|
||||||
|
|
||||||
|
@ -126,7 +127,8 @@ public class ShellyDeviceProfile {
|
||||||
hwBatchId = settings.hwinfo != null ? getString(settings.hwinfo.batchId.toString()) : "";
|
hwBatchId = settings.hwinfo != null ? getString(settings.hwinfo.batchId.toString()) : "";
|
||||||
fwDate = substringBefore(settings.fw, "/");
|
fwDate = substringBefore(settings.fw, "/");
|
||||||
fwVersion = extractFwVersion(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;
|
discoverable = (settings.discoverable == null) || settings.discoverable;
|
||||||
|
|
||||||
inColor = isLight && mode.equalsIgnoreCase(SHELLY_MODE_COLOR);
|
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);
|
logger.trace("{}: Checking for trigger, button-type[{}] is {}", thingName, idx, btnType);
|
||||||
return btnType.equalsIgnoreCase(SHELLY_BTNT_MOMENTARY) || btnType.equalsIgnoreCase(SHELLY_BTNT_MOM_ON_RELEASE)
|
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) {
|
public int getRollerFav(int id) {
|
||||||
|
@ -327,9 +330,12 @@ public class ShellyDeviceProfile {
|
||||||
|
|
||||||
public static String extractFwVersion(@Nullable String version) {
|
public static String extractFwVersion(@Nullable String version) {
|
||||||
if (version != null) {
|
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()) {
|
if (matcher.find()) {
|
||||||
// e.g. 20210226-091047/v1.10.0-rc2-89-g623b41ec0-master
|
|
||||||
return matcher.group(0);
|
return matcher.group(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,6 +32,7 @@ import org.eclipse.jetty.http.HttpHeader;
|
||||||
import org.eclipse.jetty.http.HttpMethod;
|
import org.eclipse.jetty.http.HttpMethod;
|
||||||
import org.eclipse.jetty.http.HttpStatus;
|
import org.eclipse.jetty.http.HttpStatus;
|
||||||
import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellyControlRoller;
|
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.ShellySendKeyList;
|
||||||
import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySenseKeyCode;
|
import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySenseKeyCode;
|
||||||
import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsDevice;
|
import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsDevice;
|
||||||
|
@ -90,6 +91,14 @@ public class ShellyHttpApi {
|
||||||
return callApi(SHELLY_URL_DEVINFO, ShellySettingsDevice.class);
|
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
|
* Initialize the device profile
|
||||||
*
|
*
|
||||||
|
@ -241,6 +250,17 @@ public class ShellyHttpApi {
|
||||||
ShellySettingsLogin.class);
|
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 {
|
public ShellySettingsLogin setCoIoTPeer(String peer) throws ShellyApiException {
|
||||||
return callApi(SHELLY_URL_SETTINGS + "?coiot_enable=true&coiot_peer=" + peer, ShellySettingsLogin.class);
|
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);
|
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 {
|
public ShellySettingsUpdate firmwareUpdate(String uri) throws ShellyApiException {
|
||||||
return callApi("/ota?" + uri, ShellySettingsUpdate.class);
|
return callApi("/ota?" + uri, ShellySettingsUpdate.class);
|
||||||
}
|
}
|
||||||
|
@ -560,7 +597,8 @@ public class ShellyHttpApi {
|
||||||
if (contentResponse.getStatus() != HttpStatus.OK_200) {
|
if (contentResponse.getStatus() != HttpStatus.OK_200) {
|
||||||
throw new ShellyApiException(apiResult);
|
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);
|
throw new ShellyApiException("Unexpected response: " + response);
|
||||||
}
|
}
|
||||||
} catch (ExecutionException | InterruptedException | TimeoutException | IllegalArgumentException e) {
|
} catch (ExecutionException | InterruptedException | TimeoutException | IllegalArgumentException e) {
|
||||||
|
|
|
@ -22,6 +22,7 @@ import java.util.Map;
|
||||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
import org.eclipse.jdt.annotation.Nullable;
|
import org.eclipse.jdt.annotation.Nullable;
|
||||||
import org.openhab.binding.shelly.internal.api.ShellyDeviceProfile;
|
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.CoIotDescrBlk;
|
||||||
import org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO.CoIotDescrSen;
|
import org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO.CoIotDescrSen;
|
||||||
import org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO.CoIotSensor;
|
import org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO.CoIotSensor;
|
||||||
|
@ -50,6 +51,7 @@ public class ShellyCoIoTProtocol {
|
||||||
protected final String thingName;
|
protected final String thingName;
|
||||||
protected final ShellyBaseHandler thingHandler;
|
protected final ShellyBaseHandler thingHandler;
|
||||||
protected final ShellyDeviceProfile profile;
|
protected final ShellyDeviceProfile profile;
|
||||||
|
protected final ShellyHttpApi api;
|
||||||
protected final Map<String, CoIotDescrBlk> blkMap;
|
protected final Map<String, CoIotDescrBlk> blkMap;
|
||||||
protected final Map<String, CoIotDescrSen> sensorMap;
|
protected final Map<String, CoIotDescrSen> sensorMap;
|
||||||
private final Gson gson = new GsonBuilder().create();
|
private final Gson gson = new GsonBuilder().create();
|
||||||
|
@ -68,6 +70,7 @@ public class ShellyCoIoTProtocol {
|
||||||
this.blkMap = blkMap;
|
this.blkMap = blkMap;
|
||||||
this.sensorMap = sensorMap;
|
this.sensorMap = sensorMap;
|
||||||
this.profile = thingHandler.getProfile();
|
this.profile = thingHandler.getProfile();
|
||||||
|
this.api = thingHandler.getApi();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected boolean handleStatusUpdate(List<CoIotSensor> sensorUpdates, CoIotDescrSen sen, CoIotSensor s,
|
protected boolean handleStatusUpdate(List<CoIotSensor> sensorUpdates, CoIotDescrSen sen, CoIotSensor s,
|
||||||
|
|
|
@ -37,6 +37,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
import org.eclipse.jdt.annotation.Nullable;
|
import org.eclipse.jdt.annotation.Nullable;
|
||||||
import org.openhab.binding.shelly.internal.api.ShellyApiException;
|
import org.openhab.binding.shelly.internal.api.ShellyApiException;
|
||||||
import org.openhab.binding.shelly.internal.api.ShellyDeviceProfile;
|
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.CoIotDescrBlk;
|
||||||
import org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO.CoIotDescrSen;
|
import org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO.CoIotDescrSen;
|
||||||
import org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO.CoIotDevDescrTypeAdapter;
|
import org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO.CoIotDevDescrTypeAdapter;
|
||||||
|
@ -80,7 +81,7 @@ public class ShellyCoapHandler implements ShellyCoapListener {
|
||||||
private @Nullable CoapClient statusClient;
|
private @Nullable CoapClient statusClient;
|
||||||
private Request reqDescription = new Request(Code.GET, Type.CON);
|
private Request reqDescription = new Request(Code.GET, Type.CON);
|
||||||
private Request reqStatus = 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 int coiotPort = COIOT_PORT;
|
||||||
|
|
||||||
private long coiotMessages = 0;
|
private long coiotMessages = 0;
|
||||||
|
@ -90,11 +91,13 @@ public class ShellyCoapHandler implements ShellyCoapListener {
|
||||||
private Map<String, CoIotDescrBlk> blkMap = new LinkedHashMap<>();
|
private Map<String, CoIotDescrBlk> blkMap = new LinkedHashMap<>();
|
||||||
private Map<String, CoIotDescrSen> sensorMap = new LinkedHashMap<>();
|
private Map<String, CoIotDescrSen> sensorMap = new LinkedHashMap<>();
|
||||||
private ShellyDeviceProfile profile;
|
private ShellyDeviceProfile profile;
|
||||||
|
private ShellyHttpApi api;
|
||||||
|
|
||||||
public ShellyCoapHandler(ShellyBaseHandler thingHandler, ShellyCoapServer coapServer) {
|
public ShellyCoapHandler(ShellyBaseHandler thingHandler, ShellyCoapServer coapServer) {
|
||||||
this.thingHandler = thingHandler;
|
this.thingHandler = thingHandler;
|
||||||
this.thingName = thingHandler.thingName;
|
this.thingName = thingHandler.thingName;
|
||||||
this.profile = thingHandler.getProfile();
|
this.profile = thingHandler.getProfile();
|
||||||
|
this.api = thingHandler.getApi();
|
||||||
this.coapServer = coapServer;
|
this.coapServer = coapServer;
|
||||||
this.coiot = new ShellyCoIoTVersion2(thingName, thingHandler, blkMap, sensorMap); // Default: V2
|
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);
|
logger.warn("{}: Unable to initialize CoAP access (network error)", thingName);
|
||||||
throw new ShellyApiException("Network initialization failed");
|
throw new ShellyApiException("Network initialization failed");
|
||||||
}
|
}
|
||||||
|
|
||||||
discover();
|
discover();
|
||||||
} catch (SocketException e) {
|
} catch (SocketException e) {
|
||||||
logger.warn("{}: Unable to initialize CoAP access (socket exception) - {}", thingName, e.getMessage());
|
logger.warn("{}: Unable to initialize CoAP access (socket exception) - {}", thingName, e.getMessage());
|
||||||
|
@ -283,10 +287,10 @@ public class ShellyCoapHandler implements ShellyCoapListener {
|
||||||
coiotErrors++;
|
coiotErrors++;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!discovering) {
|
if (!updatesRequested) {
|
||||||
// Observe Status Updates
|
// Observe Status Updates
|
||||||
reqStatus = sendRequest(reqStatus, config.deviceIp, COLOIT_URI_DEVSTATUS, Type.NON);
|
reqStatus = sendRequest(reqStatus, config.deviceIp, COLOIT_URI_DEVSTATUS, Type.NON);
|
||||||
discovering = true;
|
updatesRequested = true;
|
||||||
}
|
}
|
||||||
} catch (JsonSyntaxException | IllegalArgumentException | NullPointerException e) {
|
} catch (JsonSyntaxException | IllegalArgumentException | NullPointerException e) {
|
||||||
logger.debug("{}: Unable to process CoIoT Message for payload={}", thingName, payload, 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() {
|
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);
|
reqDescription = sendRequest(reqDescription, config.deviceIp, COLOIT_URI_DEVDESC, Type.CON);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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.ShellyApiException;
|
||||||
import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO;
|
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.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.ShellySettingsDevice;
|
||||||
import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsStatus;
|
import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsStatus;
|
||||||
import org.openhab.binding.shelly.internal.api.ShellyApiResult;
|
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
|
tmpPrf.auth = devInfo.auth; // missing in /settings
|
||||||
|
|
||||||
logger.debug("{}: Initializing device {}, type {}, Hardware: Rev: {}, batch {}; Firmware: {} / {} ({})",
|
logger.debug("{}: Initializing device {}, type {}, Hardware: Rev: {}, batch {}; Firmware: {} / {}", thingName,
|
||||||
thingName, tmpPrf.hostname, tmpPrf.deviceType, tmpPrf.hwRev, tmpPrf.hwBatchId, tmpPrf.fwVersion,
|
tmpPrf.hostname, tmpPrf.deviceType, tmpPrf.hwRev, tmpPrf.hwBatchId, tmpPrf.fwVersion, tmpPrf.fwDate);
|
||||||
tmpPrf.fwDate, tmpPrf.fwId);
|
|
||||||
logger.debug("{}: Shelly settings info for {}: {}", thingName, tmpPrf.hostname, tmpPrf.settingsJson);
|
logger.debug("{}: Shelly settings info for {}: {}", thingName, tmpPrf.hostname, tmpPrf.settingsJson);
|
||||||
logger.debug("{}: Device "
|
logger.debug("{}: Device "
|
||||||
+ "hasRelays:{} (numRelays={}),isRoller:{} (numRoller={}),isDimmer:{},numMeter={},isEMeter:{})"
|
+ "hasRelays:{} (numRelays={}),isRoller:{} (numRoller={}),isDimmer:{},numMeter={},isEMeter:{})"
|
||||||
|
@ -273,7 +273,7 @@ public class ShellyBaseHandler extends BaseThingHandler implements ShellyDeviceL
|
||||||
} catch (ShellyApiException e) {
|
} catch (ShellyApiException e) {
|
||||||
logger.debug("{}: Unable to set CoIoT peer: {}", thingName, e.toString());
|
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",
|
logger.warn("{}: CoIoT peer in device settings does not point this to this host, disabling CoIoT",
|
||||||
thingName);
|
thingName);
|
||||||
config.eventsCoIoT = autoCoIoT = false;
|
config.eventsCoIoT = autoCoIoT = false;
|
||||||
|
@ -396,9 +396,15 @@ public class ShellyBaseHandler extends BaseThingHandler implements ShellyDeviceL
|
||||||
// Get profile, if refreshSettings == true reload settings from device
|
// Get profile, if refreshSettings == true reload settings from device
|
||||||
logger.trace("{}: Updating status (refreshSettings={})", thingName, refreshSettings);
|
logger.trace("{}: Updating status (refreshSettings={})", thingName, refreshSettings);
|
||||||
ShellySettingsStatus status = api.getStatus();
|
ShellySettingsStatus status = api.getStatus();
|
||||||
profile = getProfile(refreshSettings || checkRestarted(status));
|
boolean restarted = checkRestarted(status);
|
||||||
|
profile = getProfile(refreshSettings || restarted);
|
||||||
profile.status = status;
|
profile.status = status;
|
||||||
profile.updateFromStatus(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
|
// If status update was successful the thing must be online
|
||||||
setThingOnline();
|
setThingOnline();
|
||||||
|
@ -571,9 +577,6 @@ public class ShellyBaseHandler extends BaseThingHandler implements ShellyDeviceL
|
||||||
private boolean checkRestarted(ShellySettingsStatus status) {
|
private boolean checkRestarted(ShellySettingsStatus status) {
|
||||||
if (profile.isInitialized() && (status.uptime < stats.lastUptime || !profile.status.update.oldVersion.isEmpty()
|
if (profile.isInitialized() && (status.uptime < stats.lastUptime || !profile.status.update.oldVersion.isEmpty()
|
||||||
&& !status.update.oldVersion.equals(profile.status.update.oldVersion))) {
|
&& !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);
|
updateProperties(profile, status);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -799,12 +802,11 @@ public class ShellyBaseHandler extends BaseThingHandler implements ShellyDeviceL
|
||||||
try {
|
try {
|
||||||
ShellyVersionDTO version = new ShellyVersionDTO();
|
ShellyVersionDTO version = new ShellyVersionDTO();
|
||||||
if (version.checkBeta(getString(prf.fwVersion))) {
|
if (version.checkBeta(getString(prf.fwVersion))) {
|
||||||
logger.info("{}: {}", prf.hostname, messages.get("versioncheck.beta", prf.fwVersion, prf.fwDate,
|
logger.info("{}: {}", prf.hostname, messages.get("versioncheck.beta", prf.fwVersion, prf.fwDate));
|
||||||
prf.fwId, SHELLY_API_MIN_FWVERSION));
|
|
||||||
} else {
|
} else {
|
||||||
if ((version.compare(prf.fwVersion, SHELLY_API_MIN_FWVERSION) < 0) && !profile.isMotion) {
|
if ((version.compare(prf.fwVersion, SHELLY_API_MIN_FWVERSION) < 0) && !profile.isMotion) {
|
||||||
logger.warn("{}: {}", prf.hostname, messages.get("versioncheck.tooold", prf.fwVersion, prf.fwDate,
|
logger.warn("{}: {}", prf.hostname,
|
||||||
prf.fwId, SHELLY_API_MIN_FWVERSION));
|
messages.get("versioncheck.tooold", prf.fwVersion, prf.fwDate, SHELLY_API_MIN_FWVERSION));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (bindingConfig.autoCoIoT && ((version.compare(prf.fwVersion, SHELLY_API_MIN_FWCOIOT)) >= 0)
|
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_UPDATE_NEW_VERS, getString(status.update.newVersion));
|
||||||
}
|
}
|
||||||
properties.put(PROPERTY_COIOTAUTO, String.valueOf(autoCoIoT));
|
properties.put(PROPERTY_COIOTAUTO, String.valueOf(autoCoIoT));
|
||||||
properties.put(PROPERTY_COIOTREFRESH, String.valueOf(autoCoIoT));
|
|
||||||
|
|
||||||
Map<String, String> thingProperties = new TreeMap<>();
|
Map<String, String> thingProperties = new TreeMap<>();
|
||||||
for (Map.Entry<String, Object> property : properties.entrySet()) {
|
for (Map.Entry<String, Object> property : properties.entrySet()) {
|
||||||
|
@ -1142,8 +1143,7 @@ public class ShellyBaseHandler extends BaseThingHandler implements ShellyDeviceL
|
||||||
if (profile.isInitialized()) {
|
if (profile.isInitialized()) {
|
||||||
properties.put(PROPERTY_MODEL_ID, getString(profile.settings.device.type));
|
properties.put(PROPERTY_MODEL_ID, getString(profile.settings.device.type));
|
||||||
properties.put(PROPERTY_MAC_ADDRESS, profile.mac);
|
properties.put(PROPERTY_MAC_ADDRESS, profile.mac);
|
||||||
properties.put(PROPERTY_FIRMWARE_VERSION,
|
properties.put(PROPERTY_FIRMWARE_VERSION, profile.fwVersion + "/" + profile.fwDate);
|
||||||
profile.fwVersion + "/" + profile.fwDate + "(" + profile.fwId + ")");
|
|
||||||
properties.put(PROPERTY_DEV_MODE, profile.mode);
|
properties.put(PROPERTY_DEV_MODE, profile.mode);
|
||||||
properties.put(PROPERTY_NUM_RELAYS, String.valueOf(profile.numRelays));
|
properties.put(PROPERTY_NUM_RELAYS, String.valueOf(profile.numRelays));
|
||||||
properties.put(PROPERTY_NUM_ROLLERS, String.valueOf(profile.numRollers));
|
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() {
|
public Map<String, String> getStatsProp() {
|
||||||
return stats.asProperties();
|
return stats.asProperties();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String checkForUpdate() {
|
||||||
|
try {
|
||||||
|
ShellyOtaCheckResult result = api.checkForUpdate();
|
||||||
|
return result.status;
|
||||||
|
} catch (ShellyApiException e) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 "";
|
||||||
|
}
|
||||||
|
}
|
|
@ -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()) + " </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> </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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
offline.status-error-fwupgrade = Firmware upgrade in progress
|
||||||
|
|
||||||
message.versioncheck.failed = Unable to check firmware version: {0}
|
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.beta = Device is running a Beta version: {0}/{1}.
|
||||||
message.versioncheck.tooold = WARNING: Firmware might be too old, installed: {0}/{1} ({2}), required minimal {3}.
|
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.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.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.
|
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.ledPowerDisable.description = ON: The power status LED will be deactivated
|
||||||
channel-type.shelly.ledStatusDisable.label = Disable Status LED
|
channel-type.shelly.ledStatusDisable.label = Disable Status LED
|
||||||
channel-type.shelly.ledStatusDisable.description = ON: The WiFi status LED will be deactivated
|
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}
|
||||||
|
|
|
@ -28,8 +28,8 @@ config-status.error.missing-userid = Keine Benutzerkennung in der Thing Konfigur
|
||||||
|
|
||||||
# General messages
|
# General messages
|
||||||
message.versioncheck.failed = Firmware-Version konnte nicht geprüft werden: {0}
|
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.beta = Es wurde eine Betaversion erkannt: {0}/{1}.
|
||||||
message.versioncheck.tooold = ACHTUNG: Eine alte Firmware wurde erkannt: {0}/{1} ({2}), minimal erforderlich {3}.
|
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.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.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.
|
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.
|
||||||
|
|
After Width: | Height: | Size: 282 B |
After Width: | Height: | Size: 4.5 KiB |
After Width: | Height: | Size: 4.4 KiB |
After Width: | Height: | Size: 3.9 KiB |
After Width: | Height: | Size: 603 B |
After Width: | Height: | Size: 554 B |
After Width: | Height: | Size: 566 B |
After Width: | Height: | Size: 3.9 KiB |
After Width: | Height: | Size: 4.5 KiB |
After Width: | Height: | Size: 1.8 KiB |
After Width: | Height: | Size: 4.6 KiB |
After Width: | Height: | Size: 2.5 KiB |
After Width: | Height: | Size: 2.4 KiB |
After Width: | Height: | Size: 1.9 KiB |
After Width: | Height: | Size: 1.9 KiB |
After Width: | Height: | Size: 1.9 KiB |
After Width: | Height: | Size: 1.6 KiB |
After Width: | Height: | Size: 1.9 KiB |
After Width: | Height: | Size: 3.9 KiB |
After Width: | Height: | Size: 3.8 KiB |
After Width: | Height: | Size: 3.9 KiB |
After Width: | Height: | Size: 4.0 KiB |
After Width: | Height: | Size: 4.1 KiB |
After Width: | Height: | Size: 4.2 KiB |
|
@ -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>
|
||||||
|
</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/>
|
|
@ -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>
|
|
@ -0,0 +1,3 @@
|
||||||
|
<script type="text/JavaScript">
|
||||||
|
setTimeout("location.href = '${forwardLink}';",${forwardTimer});
|
||||||
|
</script>
|
|
@ -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"> </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>
|
||||||
|
<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/>
|
||||||
|
|
|
@ -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/>
|
||||||
|
|
|
@ -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>
|
|
@ -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> <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> ${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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -0,0 +1,60 @@
|
||||||
|
<div class="navigation">
|
||||||
|
<table><tr><td>
|
||||||
|
Device Filter
|
||||||
|
<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"/>
|
||||||
|
</a>
|
||||||
|
<a href="${uri}/overview?action=reset_stat">
|
||||||
|
<img src="${uri}/images/resetstat.png" class="navRefIcon" title="Reset Statistics for all Devices"/>
|
||||||
|
</a>
|
||||||
|
<a href="${uri}/overview?action=otacheck">
|
||||||
|
<img src="${uri}/images/otacheck.png" class="navRefIcon" title="Trigger Firmware Check for all Devices"/>
|
||||||
|
</a>
|
||||||
|
</td></tr></table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="overview">
|
||||||
|
<hr/>
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
<table class="devTable">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<th>S</th>
|
||||||
|
<th align="left">Name</th>
|
||||||
|
<th> </th>
|
||||||
|
<th> </th>
|
||||||
|
<th> </th>
|
||||||
|
<th> </th>
|
||||||
|
<th> </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>
|