[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>
This commit is contained in:
Markus Michels 2021-03-31 22:42:33 +02:00 committed by GitHub
parent b9d3c35732
commit 1783017be4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
55 changed files with 2790 additions and 38 deletions

View File

@ -16,6 +16,14 @@ The binding gets in sync with the next status refresh.
Refer to [Advanced Users](doc/AdvancedUsers.md) for more information on openHAB Shelly integration, e.g. firmware update, network communication or log filtering. 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 |
|----------|---------------|---------|---------|---------------------------------------------------------------------| |----------|---------------|---------|---------|---------------------------------------------------------------------|

View File

@ -0,0 +1,186 @@
# Shelly Manager
The Shelly Manager is a small extension to the binding, which provides some low level information on the Shelly Devices, but also provides some functions to manage the devices.
To open the Shelly Manage launch the following URL in your browser
- http://&lt;openHAB IP address&gt;:8080/shelly/manager or
- http://&lt;openHAB IP address&gt;:8443/shelly/manager
Maybe you need to change the port matching your setup.
Shelly Manager makes you various device insights available to get an overview of your Shellys
- Get a quick overview that all Shellys operate like expected, statistical data will help to identify issues
- Have some basic setting actions integrated, which help to do an easy setup of new Shellys added to openHAB
- Make firmware updates way easier - filter 'Update available' + integrated 2-click update
- Provide a firmware download proxy, which allows to separate your Shellys from the Internet (improved device security)
## Overview
Once the Shelly Manager is opened an overview of all Shelly devices added as a Thing are displayed.
Things which are not discovered or still site in the Inbox will not be displayed.
![](images/manager/overview.png)
You'll see a bunch of technical details, which are not available as channels or in the Thing properties.
This includes information on the device communication stability.
The statistic gives you a good overview if device communication is stable or a relevant number of timeouts need to be recovered.
In this case you should verify the WiFi coverage or other options to improve stability.
The following information is available
|Column |Description |
|--------------------|---------------------------------------------------------------------------------|
|S |Thing Status - hover over the icon to see more details |
|Name |Device name - hover over the name to get more details |
|Cloud Status Icon |Indicates the status of the Shelly Cloud feature: disabled/enabled/connected |
|MQQT Status Icon |Indicates the staus of the MQTT featured disabled/enabled/connected |
|Refresh button |Trigger a status refresh in background, maybe you need to click more than once |
|Device IP |Assigned IP address, click to open the devices 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.

View File

@ -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";

View File

@ -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;

View File

@ -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));
} }

View File

@ -29,6 +29,7 @@ import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsIn
import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsRelay; import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.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);
} }
} }

View File

@ -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) {

View File

@ -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,

View File

@ -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);
} }

View File

@ -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 "";
}
}
} }

View File

@ -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);
}
}

View File

@ -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
}
}

View File

@ -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);
}
}
}
}
}

View File

@ -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
}

View File

@ -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);
}
}

View File

@ -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 "";
}
}

View File

@ -0,0 +1,307 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.shelly.internal.manager;
import static org.openhab.binding.shelly.internal.ShellyBindingConstants.*;
import static org.openhab.binding.shelly.internal.api.ShellyDeviceProfile.extractFwVersion;
import static org.openhab.binding.shelly.internal.manager.ShellyManagerConstants.*;
import static org.openhab.binding.shelly.internal.util.ShellyUtils.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.TreeMap;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.http.HttpStatus;
import org.openhab.binding.shelly.internal.ShellyHandlerFactory;
import org.openhab.binding.shelly.internal.api.ShellyApiException;
import org.openhab.binding.shelly.internal.api.ShellyDeviceProfile;
import org.openhab.binding.shelly.internal.config.ShellyThingConfiguration;
import org.openhab.binding.shelly.internal.handler.ShellyDeviceStats;
import org.openhab.binding.shelly.internal.handler.ShellyManagerInterface;
import org.openhab.binding.shelly.internal.provider.ShellyTranslationProvider;
import org.openhab.binding.shelly.internal.util.ShellyVersionDTO;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.types.State;
import org.openhab.core.types.UnDefType;
import org.osgi.service.cm.ConfigurationAdmin;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* {@link ShellyManagerOtaPage} implements the Shelly Manager's device overview page
*
* @author Markus Michels - Initial contribution
*/
@NonNullByDefault
public class ShellyManagerOverviewPage extends ShellyManagerPage {
private final Logger logger = LoggerFactory.getLogger(ShellyManagerOverviewPage.class);
public ShellyManagerOverviewPage(ConfigurationAdmin configurationAdmin,
ShellyTranslationProvider translationProvider, HttpClient httpClient, String localIp, int localPort,
ShellyHandlerFactory handlerFactory) {
super(configurationAdmin, translationProvider, httpClient, localIp, localPort, handlerFactory);
}
@Override
public ShellyMgrResponse generateContent(String path, Map<String, String[]> parameters) throws ShellyApiException {
String filter = getUrlParm(parameters, URLPARM_FILTER).toLowerCase();
String action = getUrlParm(parameters, URLPARM_ACTION).toLowerCase();
String uidParm = getUrlParm(parameters, URLPARM_UID).toLowerCase();
logger.debug("Generating overview for {} devices", getThingHandlers().size());
String html = "";
Map<String, String> properties = new HashMap<>();
properties.put(ATTRIBUTE_METATAG, "<meta http-equiv=\"refresh\" content=\"60\" />");
properties.put(ATTRIBUTE_CSS_HEADER, loadHTML(OVERVIEW_HEADER, properties));
String deviceHtml = "";
TreeMap<String, ShellyManagerInterface> sortedMap = new TreeMap<>();
for (Map.Entry<String, ShellyManagerInterface> th : getThingHandlers().entrySet()) { // sort by Device Name
ShellyManagerInterface handler = th.getValue();
sortedMap.put(getDisplayName(handler.getThing().getProperties()), handler);
}
html = loadHTML(HEADER_HTML, properties);
html += loadHTML(OVERVIEW_HTML, properties);
int filteredDevices = 0;
for (Map.Entry<String, ShellyManagerInterface> handler : sortedMap.entrySet()) {
try {
ShellyManagerInterface th = handler.getValue();
ThingStatus status = th.getThing().getStatus();
ShellyDeviceProfile profile = th.getProfile();
String uid = getString(th.getThing().getUID().getAsString()); // handler.getKey();
if (action.equals(ACTION_REFRESH) && (uidParm.isEmpty() || uidParm.equals(uid))) {
// Refresh thing status, this is asynchronosly and takes 0-3sec
th.requestUpdates(1, true);
} else if (action.equals(ACTION_RES_STATS) && (uidParm.isEmpty() || uidParm.equals(uid))) {
th.resetStats();
} else if (action.equals(ACTION_OTACHECK) && (uidParm.isEmpty() || uidParm.equals(uid))) {
th.resetStats();
}
Map<String, String> warnings = getStatusWarnings(th);
if (applyFilter(th, filter) || (filter.equals(FILTER_ATTENTION) && !warnings.isEmpty())) {
filteredDevices++;
properties.clear();
fillProperties(properties, uid, handler.getValue());
String deviceType = getDeviceType(properties);
properties.put(ATTRIBUTE_DISPLAY_NAME, getDisplayName(properties));
properties.put(ATTRIBUTE_DEV_STATUS, fillDeviceStatus(warnings));
if (!warnings.isEmpty() && (status != ThingStatus.UNKNOWN)) {
properties.put(ATTRIBUTE_STATUS_ICON, ICON_ATTENTION);
}
if (!deviceType.equalsIgnoreCase("unknown") && (status == ThingStatus.ONLINE)) {
properties.put(ATTRIBUTE_FIRMWARE_SEL, fillFirmwareHtml(uid, deviceType, profile.mode));
properties.put(ATTRIBUTE_ACTION_LIST, fillActionHtml(th, uid));
} else {
properties.put(ATTRIBUTE_FIRMWARE_SEL, "");
properties.put(ATTRIBUTE_ACTION_LIST, "");
}
html += loadHTML(OVERVIEW_DEVICE, properties);
}
} catch (ShellyApiException e) {
logger.debug("{}: Exception", LOG_PREFIX, e);
}
}
properties.clear();
properties.put("numberDevices", "<span class=\"footerDevices\">" + "Number of devices: " + filteredDevices
+ " of " + String.valueOf(getThingHandlers().size()) + "&nbsp;</span>");
properties.put(ATTRIBUTE_CSS_FOOTER, loadHTML(OVERVIEW_FOOTER, properties));
html += deviceHtml + loadHTML(FOOTER_HTML, properties);
return new ShellyMgrResponse(fillAttributes(html, properties), HttpStatus.OK_200);
}
private String fillFirmwareHtml(String uid, String deviceType, String mode) throws ShellyApiException {
String html = "\n\t\t\t\t<select name=\"fwList\" id=\"fwList\" onchange=\"location = this.options[this.selectedIndex].value;\">\n";
html += "\t\t\t\t\t<option value=\"\" selected disabled hidden>update to</option>\n";
String pVersion = "";
String bVersion = "";
String updateUrl = SHELLY_MGR_FWUPDATE_URI + "?" + URLPARM_UID + "=" + urlEncode(uid);
try {
// Get current prod + beta version from original firmware repo
logger.debug("{}: Load firmware version list for device type {}", LOG_PREFIX, deviceType);
FwRepoEntry fw = getFirmwareRepoEntry(deviceType, mode);
pVersion = extractFwVersion(fw.version);
if (!pVersion.isEmpty()) {
html += "\t\t\t\t\t<option value=\"" + updateUrl + "&" + URLPARM_VERSION + "=" + FWPROD + "\">Release "
+ pVersion + "</option>\n";
}
bVersion = extractFwVersion(fw.beta_ver);
if (!bVersion.isEmpty()) {
html += "\t\t\t\t\t<option value=\"" + updateUrl + "&" + URLPARM_VERSION + "=" + FWBETA + "\">Beta "
+ bVersion + "</option>\n";
}
// Add those from Shelly Firmware Archive
String json = httpGet(FWREPO_ARCH_URL + "?" + URLPARM_TYPE + "=" + deviceType);
if (json.startsWith("[]")) {
// no files available for this device type
logger.debug("{}: No firmware files found for device type {}", LOG_PREFIX, deviceType);
} else {
// Create selection list
json = "{" + json.replace("[{", "\"versions\":[{") + "}"; // make it an named array
FwArchList list = getFirmwareArchiveList(deviceType);
ArrayList<FwArchEntry> versions = list.versions;
if (versions != null) {
html += "\t\t\t\t\t<option value=\"\" disabled>-- Archive:</option>\n";
for (int i = versions.size() - 1; i >= 0; i--) {
FwArchEntry e = versions.get(i);
String version = getString(e.version);
ShellyVersionDTO v = new ShellyVersionDTO();
if (!version.equalsIgnoreCase(pVersion) && !version.equalsIgnoreCase(bVersion)
&& (v.compare(version, SHELLY_API_MIN_FWCOIOT) >= 0) || version.contains("master")) {
html += "\t\t\t\t\t<option value=\"" + updateUrl + "&" + URLPARM_VERSION + "=" + version
+ "\">" + version + "</option>\n";
}
}
}
}
} catch (ShellyApiException e) {
logger.debug("{}: Unable to retrieve firmware list: {}", LOG_PREFIX, e.toString());
}
html += "\t\t\t\t\t<option class=\"select-hr\" value=\"" + SHELLY_MGR_FWUPDATE_URI + "?uid=" + uid
+ "&connection=custom\">Custom URL</option>\n";
html += "\t\t\t\t</select>\n\t\t\t";
return html;
}
private String fillActionHtml(ShellyManagerInterface handler, String uid) {
String html = "\n\t\t\t\t<select name=\"actionList\" id=\"actionList\" onchange=\"location = '"
+ SHELLY_MGR_ACTION_URI + "?uid=" + urlEncode(uid) + "&" + URLPARM_ACTION
+ "='+this.options[this.selectedIndex].value;\">\n";
html += "\t\t\t\t\t<option value=\"\" selected disabled>select</option>\n";
Map<String, String> actionList = ShellyManagerActionPage.getActions(handler.getProfile());
for (Map.Entry<String, String> a : actionList.entrySet()) {
String value = a.getValue();
String seperator = "";
if (value.startsWith("-")) {
// seperator = "class=\"select-hr\" ";
html += "\t\t\t\t\t<option class=\"select-hr\" role=\"seperator\" disabled>&nbsp;</option>\n";
value = substringAfterLast(value, "-");
}
html += "\t\t\t\t\t<option " + seperator + "value=\"" + a.getKey()
+ (value.startsWith(ACTION_NONE) ? " disabled " : "") + "\">" + value + "</option>\n";
}
html += "\t\t\t\t</select>\n\t\t\t";
return html;
}
private boolean applyFilter(ShellyManagerInterface handler, String filter) {
ThingStatus status = handler.getThing().getStatus();
ShellyDeviceProfile profile = handler.getProfile();
switch (filter) {
case FILTER_ONLINE:
return status == ThingStatus.ONLINE;
case FILTER_INACTIVE:
return status != ThingStatus.ONLINE;
case FILTER_ATTENTION:
return false;
case FILTER_UPDATE:
// return handler.getChannelValue(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_UPDATE) == OnOffType.ON;
return getBool(profile.status.hasUpdate);
case FILTER_UNPROTECTED:
return !profile.auth;
case "*":
default:
return true;
}
}
private Map<String, String> getStatusWarnings(ShellyManagerInterface handler) {
Thing thing = handler.getThing();
ThingStatus status = handler.getThing().getStatus();
ShellyDeviceStats stats = handler.getStats();
ShellyDeviceProfile profile = handler.getProfile();
ShellyThingConfiguration config = thing.getConfiguration().as(ShellyThingConfiguration.class);
TreeMap<String, String> result = new TreeMap<>();
if ((status != ThingStatus.ONLINE) && (status != ThingStatus.UNKNOWN)) {
result.put("Thing Status", status.toString());
}
State wifiSignal = handler.getChannelValue(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_RSSI);
if ((profile.alwaysOn || (profile.hasBattery && (status == ThingStatus.ONLINE)))
&& ((wifiSignal != UnDefType.NULL) && (((DecimalType) wifiSignal).intValue() < 2))) {
result.put("Weak WiFi Signal", wifiSignal.toString());
}
if (profile.hasBattery) {
State lowBattery = handler.getChannelValue(CHANNEL_GROUP_BATTERY, CHANNEL_SENSOR_BAT_LOW);
if ((lowBattery == OnOffType.ON)) {
lowBattery = handler.getChannelValue(CHANNEL_GROUP_BATTERY, CHANNEL_SENSOR_BAT_LEVEL);
result.put("Battery Low", lowBattery.toString());
}
}
if (stats.lastAlarm.equalsIgnoreCase(ALARM_TYPE_RESTARTED)) {
result.put("Device Alarm", ALARM_TYPE_RESTARTED + " (" + convertTimestamp(stats.lastAlarmTs) + ")");
}
if (getBool(profile.status.overtemperature)) {
result.put("Device Alarm", ALARM_TYPE_OVERTEMP);
}
if (getBool(profile.status.overload)) {
result.put("Device Alarm", ALARM_TYPE_OVERLOAD);
}
if (getBool(profile.status.loaderror)) {
result.put("Device Alarm", ALARM_TYPE_LOADERR);
}
if (profile.isSensor) {
State sensorError = handler.getChannelValue(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_ERROR);
if (sensorError != UnDefType.NULL) {
if (!sensorError.toString().isEmpty()) {
result.put("Device Alarm", ALARM_TYPE_SENSOR_ERROR);
}
}
}
if (profile.alwaysOn && (status == ThingStatus.ONLINE)) {
if ((config.eventsCoIoT) && (profile.settings.coiot != null)) {
if ((profile.settings.coiot.enabled != null) && !profile.settings.coiot.enabled) {
result.put("CoIoT Status", "COIOT_DISABLED");
} else if (stats.coiotMessages == 0) {
result.put("CoIoT Discovery", "NO_COIOT_DISCOVERY");
} else if (stats.coiotMessages < 2) {
result.put("CoIoT Multicast", "NO_COIOT_MULTICAST");
}
}
}
return result;
}
private String fillDeviceStatus(Map<String, String> devStatus) {
if (devStatus.isEmpty()) {
return "";
}
String result = "\t\t\t\t<tr><td colspan = \"2\">Notifications:</td></tr>";
for (Map.Entry<String, String> ds : devStatus.entrySet()) {
result += "\t\t\t\t<tr><td>" + ds.getKey() + "</td><td>" + ds.getValue() + "</td></tr>\n";
}
return result;
}
}

View File

@ -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);
}
}

View File

@ -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();
}
}
}
}

View File

@ -21,8 +21,8 @@ offline.status-error-restarted = The device has restarted and will be re-initial
offline.status-error-fwupgrade = Firmware upgrade in progress 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}

View File

@ -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.

Binary file not shown.

After

Width:  |  Height:  |  Size: 282 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 603 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 554 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 566 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@ -0,0 +1,30 @@
<div class="page">
<p class="caption">Device Action: <td>${action}</p>
<hr/>
<p/>
<table>
<tr><td>Device</td><td>${thingLabel} (${serviceName})</td></tr>
<tr><td>Device IP</td><td>${deviceIp} (WiFi ${wifiNetwork})</td></tr>
<tr><td><p/></td></tr>
</table>
<p class="top-distance20">
<span class="message">${message}</span>
<span id="actionMessage" class="message">Please wait ${refreshTimer}s while status is updated.</span>
</p>
<p style="padding-left: 6px;">
<button id="actionButton" class="button" onclick="location = '${actionUrl}'">${actionButtonLabel}</button> &nbsp;&nbsp;
</p>
<script type="text/JavaScript">
var refreshTimer = ${refreshTimer};
if (refreshTimer > 0) {
setTimeout("location.href = '${uri}/overview';",(refreshTimer+1)*1000);
document.getElementById("actionButton").style.visibility="hidden";
} else {
document.getElementById("actionMessage").style.visibility="hidden";
}
</script>
</div>
<p/>

View File

@ -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>

View File

@ -0,0 +1,3 @@
<script type="text/JavaScript">
setTimeout("location.href = '${forwardLink}';",${forwardTimer});
</script>

View File

@ -0,0 +1,50 @@
<div class="page">
<p class="caption">Firmware update</p>
<hr/>
<p/>
<table style="padding-right: 6px;">
<tr><td>Device</td><td>${serviceName}</td></tr>
<tr><td>Device Type</td><td>${modelId}</td></tr>
<tr><td>Device Mode</td><td>${deviceMode}</td></tr>
<tr><td>Device IP</td><td><a href="http://${deviceIp}" title="${deviceName}"target="_blank">${deviceIp}</a></td></tr>
<tr><td style="padding-left: 10px">Device Hardware Rev</td><td>${deviceHwRev}</td></tr>
<tr><td>WiFi</td><td>${wifiNetwork}</td></tr>
<tr><td><p/></td></tr>
<tr><td>Current firmware</td><td>${firmwareVersion}</td></tr>
<tr><td>Requested version</td><td>${version}</td></tr>
</table>
<p/>
<form style="padding: 6px 6px;">
<p>Please select connection type:</p>
<input type="radio" name="connection" id="internet" value="internet"/>
<label for="internet">Device downloads firmware directly from Internet</label><br/>
<input type="radio" name="connection" id="local" value="local"/>
<label for="local">Use openHAB as proxy (device doesn't requires Internet access)</label><br/>
<input type="radio" name="connection" id="custom" value="custom"/>
<label for="custom">Use custom URL</label>
<div style="padding-left: 38px;">
<input type="text" name = "url" id="url" value="http://" style=" padding: 3px 3px;/>
<label for="url">&nbsp;</label>
</div>
<input type="hidden" id="uid" name="uid" value="${uid}">
<input type="hidden" id="version" name="version" value="${version}">
<input type="hidden" id="update" name="update" value="yes">
<p class="top-distance20">
<button class="button">Perform Update</button>&nbsp;&nbsp;
<button type="button" class="buttonCancel" onclick="location = '${uri}/overview'">Abort</button>
</p>
<p/>
</form>
<script type="text/JavaScript">
const connection = "${connection}";
document.getElementById(connection.valueOf() === "" ? "internet" : "${connection}").checked = true;
</script>
<p/>
<p class="info">${message}</p>
</div>
<p/>

View File

@ -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/>

View File

@ -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>

View File

@ -0,0 +1,106 @@
<tr >
<td>
<div class="tooltip">
<div>
<img src="${uri}/images/status_${iconStatus}.png" class="statusIcon"/>
</div>
<div class="tooltiptext">
<table style="text-align:left; ">
<tr><td colspan = "2" class="caption2">${thingName}</td></tr>
<tr><td>Status</td><td>${thingStatus} ${thingStatusDetail}</td></tr>
<tr><td>CoIoT Status</td><td>${coiotStatus}</td></tr>
<tr><td>CoIoT Destination</td><td>${coiotDestination}</td></tr>
<tr><td>Cloud Status</td><td>${cloudStatus}</td></tr>
<tr><td>MQTT Status</td><td>${mqttStatus}</td></tr>
<tr><td>Actions skipped</td><td>${actionsSkipped}</td></tr>
<tr><td>Max Internal Temp</td><td>${maxInternalTemp}</td></tr>
<tr><td>&nbsp;<br/></td></tr>
${deviceStatus}
</table>
</div>
</div>
</td>
<td>
<div class="tooltip" style="border: 1px; border-color: white;">
<span style="white-space: nowrap;">${displayName}</span>
<div class="tooltiptext">
<table style="text-align:left; ">
<tr><td colspan = "2" class="caption2">${thingName}</td></tr>
<tr><td>Shelly Device Name</td><td>${deviceName}</td></tr>
<tr><td>Device Hardware Rev</td><td>${deviceHwRev}</td></tr>
<tr><td>Device Type</td><td>${modelId}</td></tr>
<tr><td>Device Mode</td><td>${deviceMode}</td></tr>
<tr><td>Firmware Version</td><td>${firmwareVersion}</td></tr>
<tr><td>Network Name</td><td>${serviceName}</td></tr>
<tr><td>MAC Address</td><td>${macAddress}</td></tr>
<tr><td>Discoverable</td><td>${discoverable}</td></tr>
<tr><td>WiFi Auto Recovery</td><td>${wifiAutoRecovery}</td></tr>
<tr><td>WiFi AP Roaming</td><td>${apRoamingMode}</td></tr>
<tr><td>WiFi AP Threshold</td><td>${apRoamingThreshold}</td></tr>
<tr><td>Timezone</td><td>${deviceTimezone}</td></tr>
<tr><td>Time Server</td><td>${sntpServer}</td></tr>
<tr><td>Debug Mode</td><td>${debugMode}</td></tr>
</table>
</div>
</div>
</td>
<td>
<div title = "Cloud ${cloudStatus}">
<img src="${uri}/images/cloud_${cloudStatus}.png" class="icon"/>
</div>
</td>
<td>
<div title = "MQTT ${mqttStatus}">
<img src="${uri}/images/mqtt_${mqttStatus}.png" class="icon"/>
</div>
</td>
<td>
<div title="Refresh Device Status">
<a href="${uri}/overview?action=refresh&uid=${uid}">
<img src="${uri}/images/refresh.png" class="icon"/>
</a>
</div>
</td>
<td>
<div title="Reset Device Statistic">
<a href="${uri}/action?action=reset_stat&uid=${uid}">
<img src="${uri}/images/resetstat.png" class="icon"/>
</a>
</div>
</td>
<td>
<div title="Check for new firmware">
<a href="${uri}/action?action=otacheck&uid=${uid}">
<img src="${uri}/images/otacheck.png" class="icon"/>
</a>
</div>
</td>
<td><a href="http://${deviceIp}" title="${displayName}" target="_blank">${deviceIp}</a></td>
<td>${wifiNetwork}</td>
<td >
<div title = "Cloud ${cloudStatus}">
<img src="${uri}/images/wifi${wifiSignal}.png" class="icon" alt="Signal quality: ${wifiSignal} (4=best..1=very weak)"/>
</div>
</td>
<td align="right" nowrap>&nbsp;${wifiSignalRssi}</td>
<td align="center" nowrap>${batteryLevel}</td>
<td>${heartBeat}</td>
<td>${actionList}</td>
<td nowrap>${firmwareVersion}</td>
<td align="center">${updateAvailable}</td>
<td>${firmwareSelection}</td>
<td align="right" nowrap>${uptime}</td>
<td align="center" nowrap title="Max Internal Device Temp so far: ${maxInternalTemp}">${internalTemp}</td>
<td align="right" nowrap>${devUpdatePeriod} s</td>
<td align="right">${remainingWatchdog} s</td>
<td align="right">${alarmCount}</td>
<td nowrap>
<a href="${uri}/action?action=reset_stat&uid=${uid}" title="Clear alarm">${lastAlarm}</a>
</td>
<td>${lastAlarmTs}</td>
<td align="right">${deviceRestarts}</td>
<td align="right">${timeoutErrors}</td>
<td align="right">${timeoutsRecovered}</td>
<td align="right" title="CoIoT Status: ${coiotStatus}">${coiotMessages}</td>
<td align="right">${coiotErrors}</td>
</tr>

View File

@ -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>

View File

@ -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>

View File

@ -0,0 +1,60 @@
<div class="navigation">
<table><tr><td>
Device Filter&nbsp;
<select name="thingFilter" id="thingFilter" onchange="location = '${uri}/overview?filter='+this.options[this.selectedIndex].value;">
<option value="" selected disabled>select</option>
<option value="*">All</option>
<option value="online">Online only</option>
<option value="inactive">Inactive only</option>
<option value="attention">Needs Attention</option>
<option value="update">Update available</option>
<option value="unprotected">Unprotected</option>
</select>
<a href="${uri}/overview?action=refresh">
<img src="${uri}/images/refresh.png" class="navRefIcon" title="Refresh Status for all Devices"/>&nbsp;&nbsp;
</a>
<a href="${uri}/overview?action=reset_stat">
<img src="${uri}/images/resetstat.png" class="navRefIcon" title="Reset Statistics for all Devices"/>&nbsp;&nbsp;
</a>
<a href="${uri}/overview?action=otacheck">
<img src="${uri}/images/otacheck.png" class="navRefIcon" title="Trigger Firmware Check for all Devices"/>&nbsp;&nbsp;
</a>
</td></tr></table>
</div>
<div class="overview">
&nbsp;<hr/>
&nbsp;<br/>
<table class="devTable">
<tbody>
<tr>
<th>S</th>
<th align="left">Name</th>
<th>&nbsp;</th>
<th>&nbsp;</th>
<th>&nbsp;</th>
<th>&nbsp;</th>
<th>&nbsp;</th>
<th align="left">Device IP</th>
<th align="left">WiFi Network</th>
<th colspan = "2">WiFi Signal</th>
<th>Battery Level</th>
<th align="left">Heartbeat</th>
<th>Actions</th>
<th align="left">Firmware</th>
<th>Update avail</th>
<th align="left">Versions</th>
<th>Uptime</th>
<th>Internal Temp</th>
<th>Update Period</th>
<th>Remaining Watchdog</th>
<th>Events</th>
<th>Last Event</th>
<th>Event Time</th>
<th>Device Restarts</th>
<th>Timeout Errors</th>
<th>Timeouts Recovered</th>
<th>CoIOT Messages</th>
<th>CoIOT Errors</th>
</tr>