Refer to [Advanced Users](doc/AdvancedUsers.md) for more information on openHAB Shelly integration, e.g. firmware update, network communication or log filtering.
+Also check out the [Shelly Manager](doc/ShellyManager.md), which
+- provides detailed information on your Shellys
+- helps to diagnose WiFi issues or device instabilities
+- includes some common actions and
+- simplifies firmware updates.
+
+[Shelly Manager](doc/ShellyManager.md) could also act as a firmware upgrade proxy - the device doesn't need to connect directly to the Internet, instead openHAB services as a download proxy, which improves device security.
+
## Supported Devices
| thing-type | Model | Vendor ID |
|Event Type |Description |
|-------------------|---------------------------------------------------------------------------------------------------------------|
-|SHORT_PRESSED |The button was pressed once for a short time |
-|DOUBLE_PRESSED |The button was pressed twice with short delay |
-|TRIPLE_PRESSED |The button was pressed three times with short delay |
-|LONG_PRESSED |The button was pressed for a longer time |
-|SHORT_LONG_PRESSED |A short followed by a long button push |
-|LONG_SHORT_PRESSED |A long followed by a short button push |
+|SHORT_PRESSED |The button was pressed once for a short time (lastEvent=S) |
+|DOUBLE_PRESSED |The button was pressed twice with short delay (lastEvent=SS) |
+|TRIPLE_PRESSED |The button was pressed three times with short delay (lastEvent=SSS) |
+|LONG_PRESSED |The button was pressed for a longer time (lastEvent=L) |
+|SHORT_LONG_PRESSED |A short followed by a long button push (lastEvent=SL) |
+|LONG_SHORT_PRESSED |A long followed by a short button push (lastEvent=LS) |
Check the channel definitions for the various devices to see if the device supports those events.
You could use the Shelly App to set the timing for those events.
+If you want to use those events triggering a rule:
+- If a physical switch is connected to the Shelly use the input channel(`input` or `input1`/`input2`) to trigger a rule
+- For a momentary button use the `button` trigger channel as trigger, channels `lastEvent` and `eventCount` will provide details on the event
+
### Alarms
The binding provides health monitoring functions for the device.
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 |
|----------|---------------|---------|---------|---------------------------------------------------------------------|
--- /dev/null
+# Shelly Manager
+
+The Shelly Manager is a small extension to the binding, which provides some low level information on the Shelly Devices, but also provides some functions to manage the devices.
+
+To open the Shelly Manage launch the following URL in your browser
+- http://<openHAB IP address>:8080/shelly/manager or
+- http://<openHAB IP address>:8443/shelly/manager
+
+Maybe you need to change the port matching your setup.
+
+Shelly Manager makes you various device insights available to get an overview of your Shellys
+- Get a quick overview that all Shellys operate like expected, statistical data will help to identify issues
+- Have some basic setting actions integrated, which help to do an easy setup of new Shellys added to openHAB
+- Make firmware updates way easier - filter 'Update available' + integrated 2-click update
+- Provide a firmware download proxy, which allows to separate your Shellys from the Internet (improved device security)
+
+## Overview
+
+Once the Shelly Manager is opened an overview of all Shelly devices added as a Thing are displayed.
+Things which are not discovered or still site in the Inbox will not be displayed.
+
+
+
+You'll see a bunch of technical details, which are not available as channels or in the Thing properties.
+This includes information on the device communication stability.
+The statistic gives you a good overview if device communication is stable or a relevant number of timeouts need to be recovered.
+In this case you should verify the WiFi coverage or other options to improve stability.
+
+The following information is available
+|Column |Description |
+|--------------------|---------------------------------------------------------------------------------|
+|S |Thing Status - hover over the icon to see more details |
+|Name |Device name - hover over the name to get more details |
+|Cloud Status Icon |Indicates the status of the Shelly Cloud feature: disabled/enabled/connected |
+|MQQT Status Icon |Indicates the staus of the MQTT featured disabled/enabled/connected |
+|Refresh button |Trigger a status refresh in background, maybe you need to click more than once |
+|Device IP |Assigned IP address, click to open the device’s Web UI in a separate browser tab |
+|WiFi Network |SSID of the connected WiFi network |
+|WiFi Signal |WiFi signal strength, 0=none, 4=very good |
+|Battery Level |Remaining capacity of the battery |
+|Heartbeat |Last time a response or an event was received from the device |
+|Actions |Drop down with some actions, see below |
+|Firmware |Current firmware release |
+|Update avail |yes indicates that a firmware update is available |
+|Versions |List available firmware versions: prod, beta or archived |
+|Uptime |Number of seconds since last device restart |
+|Internal Temp |Device internal temperature. Max is depending on device type. |
+|Update Period |Timeout for device refresh |
+|Remaining Watchdog |Shows number of seconds until device will go offline if no update is received |
+|Events |Increases on every event triggered by the device or the binding |
+|Last Event |Type of last event or alarm (refer README.md for details) |
+|Event Time |When was last event received |
+|Device Restarts |Number of detected restarts. This is ok on firmware updates, otherwise indicates a crash |
+|Timeout Errors |Number of API timeouts, could be an indication for an unstable connection |
+|Timeouts recovered |The binding does retries and timeouts and counts successful recoveries |
+|CoIoT Messages |Number of received CoIoT messages, must be >= 2 to indicate CoIoT working |
+|CoIoT Errors |Number of CoIoT messages, which can't be processed. >0 indicates firmware issues |
+
+The column S and Name display more information when hovering with the mouse over the entries.
+
+
+
+
+### 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.
+
+
+
+## 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
+
+| | |
+|-|-|
+||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".
+
+
+
+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.
public static final String PROPERTY_STATS_TIMEOUTS = "statsTimeoutErrors";
public static final String PROPERTY_STATS_TRECOVERED = "statsTimeoutsRecovered";
public static final String PROPERTY_COIOTAUTO = "coiotAutoEnable";
- public static final String PROPERTY_COIOTREFRESH = "coiotAutoRefresh";
// Relay
public static final String CHANNEL_GROUP_RELAY_CONTROL = "relay";
public static final String SHELLY_API_MIN_FWVERSION = "v1.5.7";// v1.5.7+
public static final String SHELLY_API_MIN_FWCOIOT = "v1.6";// v1.6.0+
public static final String SHELLY_API_FWCOIOT2 = "v1.8";// CoAP 2 with FW 1.8+
+ public static final String SHELLY_API_FW_110 = "v1.10"; // FW 1.10 or newer detected, activates some add feature
// Alarm types/messages
public static final String ALARM_TYPE_NONE = "NONE";
@SerializedName("wifi_sta1")
public ShellySettingsWiFiNetwork wifiSta1;
@SerializedName("wifirecovery_reboot_enabled")
- public Boolean wifiRecoveryReboot;
+ public Boolean wifiRecoveryReboot; // FW 1.10+
+ @SerializedName("ap_roaming")
+ public ShellyApRoaming apRoaming; // FW 1.10+
public ShellySettingsMqtt mqtt; // not used for now
public ShellySettingsSntp sntp; // not used for now
public ShellySensorSleepMode sleepMode; // FW 1.6
@SerializedName("external_power")
public Integer externalPower; // H&T FW 1.6, seems to be the same like charger for the Sense
+ public Boolean debug_enable; // FW 1.10+
public String timezone;
public Double lat;
public Integer currentPos; // current position 0..100, 100=open
}
+ public class ShellyOtaCheckResult {
+ public String status;
+ }
+
+ public class ShellyApRoaming {
+ public Boolean enabled;
+ public Integer threshold;
+ }
+
public class ShellySensorSleepMode {
public Integer period;
public String unit;
return httpCode == OK_200;
}
+ public boolean isNotFound() {
+ return httpCode == NOT_FOUND_404;
+ }
+
public boolean isHttpAccessUnauthorized() {
return (httpCode == UNAUTHORIZED_401 || response.contains(SHELLY_APIERR_UNAUTHORIZED));
}
import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsRelay;
import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsRgbwLight;
import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsStatus;
+import org.openhab.binding.shelly.internal.util.ShellyVersionDTO;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@NonNullByDefault
public class ShellyDeviceProfile {
private final Logger logger = LoggerFactory.getLogger(ShellyDeviceProfile.class);
- private final static Pattern VERSION_PATTERN = Pattern.compile("v\\d+\\.\\d+\\.\\d+");
+ private final static Pattern VERSION_PATTERN = Pattern.compile("v\\d+\\.\\d+\\.\\d+(-[a-z0-9]*)?");
public boolean initialized = false; // true when initialized
public String thingName = "";
public String deviceType = "";
+ public boolean extFeatures = false;
public String settingsJson = "";
public ShellySettingsGlobal settings = new ShellySettingsGlobal();
public String hwRev = "";
public String hwBatchId = "";
public String mac = "";
- public String fwId = "";
public String fwVersion = "";
public String fwDate = "";
hwBatchId = settings.hwinfo != null ? getString(settings.hwinfo.batchId.toString()) : "";
fwDate = substringBefore(settings.fw, "/");
fwVersion = extractFwVersion(settings.fw);
- fwId = substringAfter(settings.fw, "@");
+ ShellyVersionDTO version = new ShellyVersionDTO();
+ extFeatures = version.compare(fwVersion, SHELLY_API_FW_110) >= 0;
discoverable = (settings.discoverable == null) || settings.discoverable;
inColor = isLight && mode.equalsIgnoreCase(SHELLY_MODE_COLOR);
logger.trace("{}: Checking for trigger, button-type[{}] is {}", thingName, idx, btnType);
return btnType.equalsIgnoreCase(SHELLY_BTNT_MOMENTARY) || btnType.equalsIgnoreCase(SHELLY_BTNT_MOM_ON_RELEASE)
- || btnType.equalsIgnoreCase(SHELLY_BTNT_ONE_BUTTON) || btnType.equalsIgnoreCase(SHELLY_BTNT_TWO_BUTTON);
+ || btnType.equalsIgnoreCase(SHELLY_BTNT_ONE_BUTTON) || btnType.equalsIgnoreCase(SHELLY_BTNT_TWO_BUTTON)
+ || btnType.equalsIgnoreCase(SHELLY_BTNT_DETACHED);
}
public int getRollerFav(int id) {
public static String extractFwVersion(@Nullable String version) {
if (version != null) {
- Matcher matcher = VERSION_PATTERN.matcher(version);
+ // fix version e.g. 20210319-122304/v.1.10-Dimmer1-gfd4cc10 (with v.1. instead of v1.)
+ String vers = version.replace("/v.1.10-", "/v1.10.0-");
+
+ // Extract version from string, e.g. 20210226-091047/v1.10.0-rc2-89-g623b41ec0-master
+ Matcher matcher = VERSION_PATTERN.matcher(vers);
if (matcher.find()) {
- // e.g. 20210226-091047/v1.10.0-rc2-89-g623b41ec0-master
return matcher.group(0);
}
}
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.HttpStatus;
import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellyControlRoller;
+import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellyOtaCheckResult;
import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySendKeyList;
import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySenseKeyCode;
import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsDevice;
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
*
ShellySettingsLogin.class);
}
+ public String getCoIoTDescription() throws ShellyApiException {
+ try {
+ return callApi("/cit/d", String.class);
+ } catch (ShellyApiException e) {
+ if (e.getApiResult().isNotFound()) {
+ return ""; // only supported by FW 1.10+
+ }
+ throw e;
+ }
+ }
+
public ShellySettingsLogin setCoIoTPeer(String peer) throws ShellyApiException {
return callApi(SHELLY_URL_SETTINGS + "?coiot_enable=true&coiot_peer=" + peer, ShellySettingsLogin.class);
}
return callApi(SHELLY_URL_SETTINGS + "?reset=true", String.class);
}
+ public ShellyOtaCheckResult checkForUpdate() throws ShellyApiException {
+ return callApi("/ota/check", ShellyOtaCheckResult.class); // nw FW 1.10+: trigger update check
+ }
+
+ public String setWiFiRecovery(boolean enable) throws ShellyApiException {
+ return callApi(SHELLY_URL_SETTINGS + "?wifirecovery_reboot_enabled=" + (enable ? "true" : "false"),
+ String.class); // FW 1.10+: Enable auto-restart on WiFi problems
+ }
+
+ public String setApRoaming(boolean enable) throws ShellyApiException { // FW 1.10+: Enable AP Roadming
+ return callApi(SHELLY_URL_SETTINGS + "?ap_roaming_enabled=" + (enable ? "true" : "false"), String.class);
+ }
+
+ public String resetStaCache() throws ShellyApiException { // FW 1.10+: Reset cached STA/AP list and to a rescan
+ return callApi("/sta_cache_reset", String.class);
+ }
+
public ShellySettingsUpdate firmwareUpdate(String uri) throws ShellyApiException {
return callApi("/ota?" + uri, ShellySettingsUpdate.class);
}
if (contentResponse.getStatus() != HttpStatus.OK_200) {
throw new ShellyApiException(apiResult);
}
- if (response.isEmpty() || !response.startsWith("{") && !response.startsWith("[")) {
+ if (response.isEmpty() || !response.startsWith("{") && !response.startsWith("[") && !url.contains("/debug/")
+ && !url.contains("/sta_cache_reset")) {
throw new ShellyApiException("Unexpected response: " + response);
}
} catch (ExecutionException | InterruptedException | TimeoutException | IllegalArgumentException e) {
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.shelly.internal.api.ShellyDeviceProfile;
+import org.openhab.binding.shelly.internal.api.ShellyHttpApi;
import org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO.CoIotDescrBlk;
import org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO.CoIotDescrSen;
import org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO.CoIotSensor;
protected final String thingName;
protected final ShellyBaseHandler thingHandler;
protected final ShellyDeviceProfile profile;
+ protected final ShellyHttpApi api;
protected final Map<String, CoIotDescrBlk> blkMap;
protected final Map<String, CoIotDescrSen> sensorMap;
private final Gson gson = new GsonBuilder().create();
this.blkMap = blkMap;
this.sensorMap = sensorMap;
this.profile = thingHandler.getProfile();
+ this.api = thingHandler.getApi();
}
protected boolean handleStatusUpdate(List<CoIotSensor> sensorUpdates, CoIotDescrSen sen, CoIotSensor s,
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.shelly.internal.api.ShellyApiException;
import org.openhab.binding.shelly.internal.api.ShellyDeviceProfile;
+import org.openhab.binding.shelly.internal.api.ShellyHttpApi;
import org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO.CoIotDescrBlk;
import org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO.CoIotDescrSen;
import org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO.CoIotDevDescrTypeAdapter;
private @Nullable CoapClient statusClient;
private Request reqDescription = new Request(Code.GET, Type.CON);
private Request reqStatus = new Request(Code.GET, Type.CON);
- private boolean discovering = false;
+ private boolean updatesRequested = false;
private int coiotPort = COIOT_PORT;
private long coiotMessages = 0;
private Map<String, CoIotDescrBlk> blkMap = new LinkedHashMap<>();
private Map<String, CoIotDescrSen> sensorMap = new LinkedHashMap<>();
private ShellyDeviceProfile profile;
+ private ShellyHttpApi api;
public ShellyCoapHandler(ShellyBaseHandler thingHandler, ShellyCoapServer coapServer) {
this.thingHandler = thingHandler;
this.thingName = thingHandler.thingName;
this.profile = thingHandler.getProfile();
+ this.api = thingHandler.getApi();
this.coapServer = coapServer;
this.coiot = new ShellyCoIoTVersion2(thingName, thingHandler, blkMap, sensorMap); // Default: V2
logger.warn("{}: Unable to initialize CoAP access (network error)", thingName);
throw new ShellyApiException("Network initialization failed");
}
+
discover();
} catch (SocketException e) {
logger.warn("{}: Unable to initialize CoAP access (socket exception) - {}", thingName, e.getMessage());
coiotErrors++;
}
- if (!discovering) {
+ if (!updatesRequested) {
// Observe Status Updates
reqStatus = sendRequest(reqStatus, config.deviceIp, COLOIT_URI_DEVSTATUS, Type.NON);
- discovering = true;
+ updatesRequested = true;
}
} catch (JsonSyntaxException | IllegalArgumentException | NullPointerException e) {
logger.debug("{}: Unable to process CoIoT Message for payload={}", thingName, payload, e);
}
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);
}
import org.openhab.binding.shelly.internal.api.ShellyApiException;
import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO;
import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellyInputState;
+import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellyOtaCheckResult;
import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsDevice;
import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsStatus;
import org.openhab.binding.shelly.internal.api.ShellyApiResult;
}
tmpPrf.auth = devInfo.auth; // missing in /settings
- logger.debug("{}: Initializing device {}, type {}, Hardware: Rev: {}, batch {}; Firmware: {} / {} ({})",
- thingName, tmpPrf.hostname, tmpPrf.deviceType, tmpPrf.hwRev, tmpPrf.hwBatchId, tmpPrf.fwVersion,
- tmpPrf.fwDate, tmpPrf.fwId);
+ logger.debug("{}: Initializing device {}, type {}, Hardware: Rev: {}, batch {}; Firmware: {} / {}", thingName,
+ tmpPrf.hostname, tmpPrf.deviceType, tmpPrf.hwRev, tmpPrf.hwBatchId, tmpPrf.fwVersion, tmpPrf.fwDate);
logger.debug("{}: Shelly settings info for {}: {}", thingName, tmpPrf.hostname, tmpPrf.settingsJson);
logger.debug("{}: Device "
+ "hasRelays:{} (numRelays={}),isRoller:{} (numRoller={}),isDimmer:{},numMeter={},isEMeter:{})"
} catch (ShellyApiException e) {
logger.debug("{}: Unable to set CoIoT peer: {}", thingName, e.toString());
}
- } else if (!devpeer.equals(ourpeer)) {
+ } else if (!devpeer.isEmpty() && !devpeer.equals(ourpeer)) {
logger.warn("{}: CoIoT peer in device settings does not point this to this host, disabling CoIoT",
thingName);
config.eventsCoIoT = autoCoIoT = false;
// Get profile, if refreshSettings == true reload settings from device
logger.trace("{}: Updating status (refreshSettings={})", thingName, refreshSettings);
ShellySettingsStatus status = api.getStatus();
- profile = getProfile(refreshSettings || checkRestarted(status));
+ boolean restarted = checkRestarted(status);
+ profile = getProfile(refreshSettings || restarted);
profile.status = status;
profile.updateFromStatus(status);
+ if (restarted) {
+ logger.debug("{}: Device restart #{} detected", thingName, stats.restarts);
+ stats.restarts++;
+ postEvent(ALARM_TYPE_RESTARTED, true);
+ }
// If status update was successful the thing must be online
setThingOnline();
private boolean checkRestarted(ShellySettingsStatus status) {
if (profile.isInitialized() && (status.uptime < stats.lastUptime || !profile.status.update.oldVersion.isEmpty()
&& !status.update.oldVersion.equals(profile.status.update.oldVersion))) {
- logger.debug("{}: Device restart #{} detected", thingName, stats.restarts);
- stats.restarts++;
- postEvent(ALARM_TYPE_RESTARTED, true);
updateProperties(profile, status);
return true;
}
try {
ShellyVersionDTO version = new ShellyVersionDTO();
if (version.checkBeta(getString(prf.fwVersion))) {
- logger.info("{}: {}", prf.hostname, messages.get("versioncheck.beta", prf.fwVersion, prf.fwDate,
- prf.fwId, SHELLY_API_MIN_FWVERSION));
+ logger.info("{}: {}", prf.hostname, messages.get("versioncheck.beta", prf.fwVersion, prf.fwDate));
} else {
if ((version.compare(prf.fwVersion, SHELLY_API_MIN_FWVERSION) < 0) && !profile.isMotion) {
- logger.warn("{}: {}", prf.hostname, messages.get("versioncheck.tooold", prf.fwVersion, prf.fwDate,
- prf.fwId, SHELLY_API_MIN_FWVERSION));
+ logger.warn("{}: {}", prf.hostname,
+ messages.get("versioncheck.tooold", prf.fwVersion, prf.fwDate, SHELLY_API_MIN_FWVERSION));
}
}
if (bindingConfig.autoCoIoT && ((version.compare(prf.fwVersion, SHELLY_API_MIN_FWCOIOT)) >= 0)
properties.put(PROPERTY_UPDATE_NEW_VERS, getString(status.update.newVersion));
}
properties.put(PROPERTY_COIOTAUTO, String.valueOf(autoCoIoT));
- properties.put(PROPERTY_COIOTREFRESH, String.valueOf(autoCoIoT));
Map<String, String> thingProperties = new TreeMap<>();
for (Map.Entry<String, Object> property : properties.entrySet()) {
if (profile.isInitialized()) {
properties.put(PROPERTY_MODEL_ID, getString(profile.settings.device.type));
properties.put(PROPERTY_MAC_ADDRESS, profile.mac);
- properties.put(PROPERTY_FIRMWARE_VERSION,
- profile.fwVersion + "/" + profile.fwDate + "(" + profile.fwId + ")");
+ properties.put(PROPERTY_FIRMWARE_VERSION, profile.fwVersion + "/" + profile.fwDate);
properties.put(PROPERTY_DEV_MODE, profile.mode);
properties.put(PROPERTY_NUM_RELAYS, String.valueOf(profile.numRelays));
properties.put(PROPERTY_NUM_ROLLERS, String.valueOf(profile.numRollers));
public Map<String, String> getStatsProp() {
return stats.asProperties();
}
+
+ public String checkForUpdate() {
+ try {
+ ShellyOtaCheckResult result = api.checkForUpdate();
+ return result.status;
+ } catch (ShellyApiException e) {
+ return "";
+ }
+ }
}
--- /dev/null
+/**
+ * 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);
+ }
+}
--- /dev/null
+/**
+ * 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
+ }
+}
--- /dev/null
+/**
+ * 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);
+ }
+ }
+ }
+ }
+}
--- /dev/null
+/**
+ * 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
+}
--- /dev/null
+/**
+ * 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);
+ }
+}
--- /dev/null
+/**
+ * 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 "";
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.shelly.internal.manager;
+
+import static org.openhab.binding.shelly.internal.ShellyBindingConstants.*;
+import static org.openhab.binding.shelly.internal.api.ShellyDeviceProfile.extractFwVersion;
+import static org.openhab.binding.shelly.internal.manager.ShellyManagerConstants.*;
+import static org.openhab.binding.shelly.internal.util.ShellyUtils.*;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.TreeMap;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jetty.client.HttpClient;
+import org.eclipse.jetty.http.HttpStatus;
+import org.openhab.binding.shelly.internal.ShellyHandlerFactory;
+import org.openhab.binding.shelly.internal.api.ShellyApiException;
+import org.openhab.binding.shelly.internal.api.ShellyDeviceProfile;
+import org.openhab.binding.shelly.internal.config.ShellyThingConfiguration;
+import org.openhab.binding.shelly.internal.handler.ShellyDeviceStats;
+import org.openhab.binding.shelly.internal.handler.ShellyManagerInterface;
+import org.openhab.binding.shelly.internal.provider.ShellyTranslationProvider;
+import org.openhab.binding.shelly.internal.util.ShellyVersionDTO;
+import org.openhab.core.library.types.DecimalType;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.types.State;
+import org.openhab.core.types.UnDefType;
+import org.osgi.service.cm.ConfigurationAdmin;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * {@link ShellyManagerOtaPage} implements the Shelly Manager's device overview page
+ *
+ * @author Markus Michels - Initial contribution
+ */
+@NonNullByDefault
+public class ShellyManagerOverviewPage extends ShellyManagerPage {
+ private final Logger logger = LoggerFactory.getLogger(ShellyManagerOverviewPage.class);
+
+ public ShellyManagerOverviewPage(ConfigurationAdmin configurationAdmin,
+ ShellyTranslationProvider translationProvider, HttpClient httpClient, String localIp, int localPort,
+ ShellyHandlerFactory handlerFactory) {
+ super(configurationAdmin, translationProvider, httpClient, localIp, localPort, handlerFactory);
+ }
+
+ @Override
+ public ShellyMgrResponse generateContent(String path, Map<String, String[]> parameters) throws ShellyApiException {
+ String filter = getUrlParm(parameters, URLPARM_FILTER).toLowerCase();
+ String action = getUrlParm(parameters, URLPARM_ACTION).toLowerCase();
+ String uidParm = getUrlParm(parameters, URLPARM_UID).toLowerCase();
+
+ logger.debug("Generating overview for {}Â devices", getThingHandlers().size());
+
+ String html = "";
+ Map<String, String> properties = new HashMap<>();
+ properties.put(ATTRIBUTE_METATAG, "<meta http-equiv=\"refresh\" content=\"60\" />");
+ properties.put(ATTRIBUTE_CSS_HEADER, loadHTML(OVERVIEW_HEADER, properties));
+
+ String deviceHtml = "";
+ TreeMap<String, ShellyManagerInterface> sortedMap = new TreeMap<>();
+ for (Map.Entry<String, ShellyManagerInterface> th : getThingHandlers().entrySet()) { // sort by Device Name
+ ShellyManagerInterface handler = th.getValue();
+ sortedMap.put(getDisplayName(handler.getThing().getProperties()), handler);
+ }
+
+ html = loadHTML(HEADER_HTML, properties);
+ html += loadHTML(OVERVIEW_HTML, properties);
+
+ int filteredDevices = 0;
+ for (Map.Entry<String, ShellyManagerInterface> handler : sortedMap.entrySet()) {
+ try {
+ ShellyManagerInterface th = handler.getValue();
+ ThingStatus status = th.getThing().getStatus();
+ ShellyDeviceProfile profile = th.getProfile();
+ String uid = getString(th.getThing().getUID().getAsString()); // handler.getKey();
+
+ if (action.equals(ACTION_REFRESH) && (uidParm.isEmpty() || uidParm.equals(uid))) {
+ // Refresh thing status, this is asynchronosly and takes 0-3sec
+ th.requestUpdates(1, true);
+ } else if (action.equals(ACTION_RES_STATS) && (uidParm.isEmpty() || uidParm.equals(uid))) {
+ th.resetStats();
+ } else if (action.equals(ACTION_OTACHECK) && (uidParm.isEmpty() || uidParm.equals(uid))) {
+ th.resetStats();
+ }
+
+ Map<String, String> warnings = getStatusWarnings(th);
+ if (applyFilter(th, filter) || (filter.equals(FILTER_ATTENTION) && !warnings.isEmpty())) {
+ filteredDevices++;
+ properties.clear();
+ fillProperties(properties, uid, handler.getValue());
+ String deviceType = getDeviceType(properties);
+
+ properties.put(ATTRIBUTE_DISPLAY_NAME, getDisplayName(properties));
+ properties.put(ATTRIBUTE_DEV_STATUS, fillDeviceStatus(warnings));
+ if (!warnings.isEmpty() && (status != ThingStatus.UNKNOWN)) {
+ properties.put(ATTRIBUTE_STATUS_ICON, ICON_ATTENTION);
+ }
+ if (!deviceType.equalsIgnoreCase("unknown") && (status == ThingStatus.ONLINE)) {
+ properties.put(ATTRIBUTE_FIRMWARE_SEL, fillFirmwareHtml(uid, deviceType, profile.mode));
+ properties.put(ATTRIBUTE_ACTION_LIST, fillActionHtml(th, uid));
+ } else {
+ properties.put(ATTRIBUTE_FIRMWARE_SEL, "");
+ properties.put(ATTRIBUTE_ACTION_LIST, "");
+ }
+ html += loadHTML(OVERVIEW_DEVICE, properties);
+ }
+ } catch (ShellyApiException e) {
+ logger.debug("{}: Exception", LOG_PREFIX, e);
+ }
+ }
+
+ properties.clear();
+ properties.put("numberDevices", "<span class=\"footerDevices\">" + "Number of devices: " + filteredDevices
+ + " of " + String.valueOf(getThingHandlers().size()) + " </span>");
+ properties.put(ATTRIBUTE_CSS_FOOTER, loadHTML(OVERVIEW_FOOTER, properties));
+ html += deviceHtml + loadHTML(FOOTER_HTML, properties);
+ return new ShellyMgrResponse(fillAttributes(html, properties), HttpStatus.OK_200);
+ }
+
+ private String fillFirmwareHtml(String uid, String deviceType, String mode) throws ShellyApiException {
+ String html = "\n\t\t\t\t<select name=\"fwList\" id=\"fwList\" onchange=\"location = this.options[this.selectedIndex].value;\">\n";
+ html += "\t\t\t\t\t<option value=\"\" selected disabled hidden>update to</option>\n";
+
+ String pVersion = "";
+ String bVersion = "";
+ String updateUrl = SHELLY_MGR_FWUPDATE_URI + "?" + URLPARM_UID + "=" + urlEncode(uid);
+ try {
+ // Get current prod + beta version from original firmware repo
+ logger.debug("{}: Load firmware version list for device type {}", LOG_PREFIX, deviceType);
+ FwRepoEntry fw = getFirmwareRepoEntry(deviceType, mode);
+ pVersion = extractFwVersion(fw.version);
+ if (!pVersion.isEmpty()) {
+ html += "\t\t\t\t\t<option value=\"" + updateUrl + "&" + URLPARM_VERSION + "=" + FWPROD + "\">Release "
+ + pVersion + "</option>\n";
+ }
+ bVersion = extractFwVersion(fw.beta_ver);
+ if (!bVersion.isEmpty()) {
+ html += "\t\t\t\t\t<option value=\"" + updateUrl + "&" + URLPARM_VERSION + "=" + FWBETA + "\">Beta "
+ + bVersion + "</option>\n";
+ }
+
+ // Add those from Shelly Firmware Archive
+ String json = httpGet(FWREPO_ARCH_URL + "?" + URLPARM_TYPE + "=" + deviceType);
+ if (json.startsWith("[]")) {
+ // no files available for this device type
+ logger.debug("{}: No firmware files found for device type {}", LOG_PREFIX, deviceType);
+ } else {
+ // Create selection list
+ json = "{" + json.replace("[{", "\"versions\":[{") + "}"; // make it an named array
+ FwArchList list = getFirmwareArchiveList(deviceType);
+ ArrayList<FwArchEntry> versions = list.versions;
+ if (versions != null) {
+ html += "\t\t\t\t\t<option value=\"\" disabled>-- Archive:</option>\n";
+ for (int i = versions.size() - 1; i >= 0; i--) {
+ FwArchEntry e = versions.get(i);
+ String version = getString(e.version);
+ ShellyVersionDTO v = new ShellyVersionDTO();
+ if (!version.equalsIgnoreCase(pVersion) && !version.equalsIgnoreCase(bVersion)
+ && (v.compare(version, SHELLY_API_MIN_FWCOIOT) >= 0) || version.contains("master")) {
+ html += "\t\t\t\t\t<option value=\"" + updateUrl + "&" + URLPARM_VERSION + "=" + version
+ + "\">" + version + "</option>\n";
+ }
+ }
+ }
+ }
+ } catch (ShellyApiException e) {
+ logger.debug("{}: Unable to retrieve firmware list: {}", LOG_PREFIX, e.toString());
+ }
+
+ html += "\t\t\t\t\t<option class=\"select-hr\" value=\"" + SHELLY_MGR_FWUPDATE_URI + "?uid=" + uid
+ + "&connection=custom\">Custom URL</option>\n";
+
+ html += "\t\t\t\t</select>\n\t\t\t";
+
+ return html;
+ }
+
+ private String fillActionHtml(ShellyManagerInterface handler, String uid) {
+ String html = "\n\t\t\t\t<select name=\"actionList\" id=\"actionList\" onchange=\"location = '"
+ + SHELLY_MGR_ACTION_URI + "?uid=" + urlEncode(uid) + "&" + URLPARM_ACTION
+ + "='+this.options[this.selectedIndex].value;\">\n";
+ html += "\t\t\t\t\t<option value=\"\" selected disabled>select</option>\n";
+
+ Map<String, String> actionList = ShellyManagerActionPage.getActions(handler.getProfile());
+ for (Map.Entry<String, String> a : actionList.entrySet()) {
+ String value = a.getValue();
+ String seperator = "";
+ if (value.startsWith("-")) {
+ // seperator = "class=\"select-hr\" ";
+ html += "\t\t\t\t\t<option class=\"select-hr\" role=\"seperator\" disabled> </option>\n";
+ value = substringAfterLast(value, "-");
+ }
+ html += "\t\t\t\t\t<option " + seperator + "value=\"" + a.getKey()
+ + (value.startsWith(ACTION_NONE) ? " disabled " : "") + "\">" + value + "</option>\n";
+ }
+ html += "\t\t\t\t</select>\n\t\t\t";
+ return html;
+ }
+
+ private boolean applyFilter(ShellyManagerInterface handler, String filter) {
+ ThingStatus status = handler.getThing().getStatus();
+ ShellyDeviceProfile profile = handler.getProfile();
+
+ switch (filter) {
+ case FILTER_ONLINE:
+ return status == ThingStatus.ONLINE;
+ case FILTER_INACTIVE:
+ return status != ThingStatus.ONLINE;
+ case FILTER_ATTENTION:
+ return false;
+ case FILTER_UPDATE:
+ // return handler.getChannelValue(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_UPDATE) == OnOffType.ON;
+ return getBool(profile.status.hasUpdate);
+ case FILTER_UNPROTECTED:
+ return !profile.auth;
+ case "*":
+ default:
+ return true;
+ }
+ }
+
+ private Map<String, String> getStatusWarnings(ShellyManagerInterface handler) {
+ Thing thing = handler.getThing();
+ ThingStatus status = handler.getThing().getStatus();
+ ShellyDeviceStats stats = handler.getStats();
+ ShellyDeviceProfile profile = handler.getProfile();
+ ShellyThingConfiguration config = thing.getConfiguration().as(ShellyThingConfiguration.class);
+ TreeMap<String, String> result = new TreeMap<>();
+
+ if ((status != ThingStatus.ONLINE) && (status != ThingStatus.UNKNOWN)) {
+ result.put("Thing Status", status.toString());
+ }
+ State wifiSignal = handler.getChannelValue(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_RSSI);
+ if ((profile.alwaysOn || (profile.hasBattery && (status == ThingStatus.ONLINE)))
+ && ((wifiSignal != UnDefType.NULL) && (((DecimalType) wifiSignal).intValue() < 2))) {
+ result.put("Weak WiFi Signal", wifiSignal.toString());
+ }
+ if (profile.hasBattery) {
+ State lowBattery = handler.getChannelValue(CHANNEL_GROUP_BATTERY, CHANNEL_SENSOR_BAT_LOW);
+ if ((lowBattery == OnOffType.ON)) {
+ lowBattery = handler.getChannelValue(CHANNEL_GROUP_BATTERY, CHANNEL_SENSOR_BAT_LEVEL);
+ result.put("Battery Low", lowBattery.toString());
+ }
+ }
+
+ if (stats.lastAlarm.equalsIgnoreCase(ALARM_TYPE_RESTARTED)) {
+ result.put("Device Alarm", ALARM_TYPE_RESTARTED + " (" + convertTimestamp(stats.lastAlarmTs) + ")");
+ }
+ if (getBool(profile.status.overtemperature)) {
+ result.put("Device Alarm", ALARM_TYPE_OVERTEMP);
+ }
+ if (getBool(profile.status.overload)) {
+ result.put("Device Alarm", ALARM_TYPE_OVERLOAD);
+ }
+ if (getBool(profile.status.loaderror)) {
+ result.put("Device Alarm", ALARM_TYPE_LOADERR);
+ }
+ if (profile.isSensor) {
+ State sensorError = handler.getChannelValue(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_ERROR);
+ if (sensorError != UnDefType.NULL) {
+ if (!sensorError.toString().isEmpty()) {
+ result.put("Device Alarm", ALARM_TYPE_SENSOR_ERROR);
+ }
+ }
+ }
+ if (profile.alwaysOn && (status == ThingStatus.ONLINE)) {
+ if ((config.eventsCoIoT) && (profile.settings.coiot != null)) {
+ if ((profile.settings.coiot.enabled != null) && !profile.settings.coiot.enabled) {
+ result.put("CoIoT Status", "COIOT_DISABLED");
+ } else if (stats.coiotMessages == 0) {
+ result.put("CoIoT Discovery", "NO_COIOT_DISCOVERY");
+ } else if (stats.coiotMessages < 2) {
+ result.put("CoIoT Multicast", "NO_COIOT_MULTICAST");
+ }
+ }
+ }
+
+ return result;
+ }
+
+ private String fillDeviceStatus(Map<String, String> devStatus) {
+ if (devStatus.isEmpty()) {
+ return "";
+ }
+
+ String result = "\t\t\t\t<tr><td colspan = \"2\">Notifications:</td></tr>";
+ for (Map.Entry<String, String> ds : devStatus.entrySet()) {
+ result += "\t\t\t\t<tr><td>" + ds.getKey() + "</td><td>" + ds.getValue() + "</td></tr>\n";
+ }
+ return result;
+ }
+}
--- /dev/null
+/**
+ * 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);
+ }
+}
--- /dev/null
+/**
+ * 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();
+ }
+ }
+ }
+}
offline.status-error-fwupgrade = Firmware upgrade in progress
message.versioncheck.failed = Unable to check firmware version: {0}
-message.versioncheck.beta = Device is running a Beta version: {0}/{1} ({2}),make sure this is newer than {3} release build.
-message.versioncheck.tooold = WARNING: Firmware might be too old, installed: {0}/{1} ({2}), required minimal {3}.
+message.versioncheck.beta = Device is running a Beta version: {0}/{1}.
+message.versioncheck.tooold = WARNING: Firmware might be too old, installed: {0}/{1}, required minimal {3}.
message.versioncheck.update = INFO: New firmware available: current version: {0}, new version: {1}
message.versioncheck.autocoiot = INFO: Firmware is full-filling the minimum version to auto-enable CoIoT
message.roller.calibrating = Device is not calibrated, use Shelly App to perform initial roller calibration.
channel-type.shelly.ledPowerDisable.description = ON: The power status LED will be deactivated
channel-type.shelly.ledStatusDisable.label = Disable Status LED
channel-type.shelly.ledStatusDisable.description = ON: The WiFi status LED will be deactivated
+
+# Shelly Manager
+message.manager.invalid-url = Invalid URL or syntax
+
+message.manager.buttons.ok = OK
+message.manager.buttons.abort = Abort
+
+message.manager.action.unknown = Action {0} is unknown
+message.manager.action.reset-stats = Reset Statistics
+message.manager.action.restart = Reboot Device
+message.manager.action.restart.info = The device is restarting and reconnects to WiFi. It will take a moment until device status is refreshed in openHAB.
+message.manager.action.restart.confirm = The device will restart and reconnects to WiFi.
+message.manager.action.resstats.confirm = Device statistics and alarm has been reset.
+message.manager.action.setcloud.config = Cloud function is now {0}.
+message.manager.action.protect = Protect Device
+message.manager.action.protect.id-missing = Credentials for device access are not configured, go to Shelly Binding Settings and provide user id and password.<br/>You could use the 'Protect' action to apply this configuration to the device.
+message.manager.action.protect.status = Device protection is currently {0}. User id {1} is required to access the device.
+message.manager.action.protect.new = Device login will be set to user {0} with password {1}.
+message.manager.action.protect.confirm = Device login was updated to user {0} with password {1}.
+message.manager.action.could-enable = Enable Cloud
+message.manager.action.could-disable = Disable Cloud
+message.manager.action.coiot-mcast = Set CoIoT Multicast
+message.manager.action.coiot-peer = Set CoIoT Peer
+message.manager.action.timezone = Set Timezone
+message.manager.action.reset = Factory Reset
+message.manager.action.reset.warning = Attention: Performing this action will reset the device to factory defaults.<br/>All configuration data incl. WiFi settings get lost and device will return to Access Point mode (WiFi {0})
+message.manager.action.reset.confirm = Factory reset was performed. Connect to WiFi network {0} and open http://192.168.33.1 to restart with device setup.
+message.manager.action.checkupd.new = Firmware update available: {0}
+message.manager.action.checkupd.ok = Firmware check completed, check device overview for new version.
+message.manager.action.checkupd.runnuing = Firmware check was initiated.
+message.manager.action.checkupd.failed = Unable to check for firmware update: {0}
+message.manager.action.setwifirec-enable = The device performs an auto-restart if WiFi Recovery Mode is enabled and device is facing WiFi connectivity issues.
+message.manager.action.setwifirec-disable = WiFi Recovery Mode will be disabled.
+message.manager.action.setwifirec-confirm = WiFi Recovery Mode has been {0}.
+message.manager.action.setwifirec-failed = Unable to update setting for WiFi Recovery Mode: {0}
+message.manager.action.aproaming-enable = WiFi Access Point Roaming will be enabled. Check product documentation for details.
+message.manager.action.aproaming-disable = WiFi Access Point Roaming will be disabled.
+message.manager.action.aproaming-confirm = Unable to update setting WiFi Access Point Roaming: {0}
+message.manager.action.aproaming-failed = Unable to update setting for WiFi Recovery Mode: {0}
+message.manager.action.resetsta-info = The WiFi STA/AP Cache will be cleared and the device reconnects to the strongest Access Point.
+message.manager.action.resetsta-confirm = Device is reconnecting to the strongest WiFi Access Point.
+message.manager.action.resetsta-failed = Unable to clear STA/AP list and reconnect to WiFi: {0}
+message.manager.action.debug-enable = Device Debug will be enabled. Use this feature only if requested by Allterco Support.
+message.manager.action.debug-disable = Device Debug will be disabled.
+message.manager.action.debug-confirm = Device Debug was {0}.
+message.manager.action.getdebug-failed = Unable to get Debug Log: {0}
+
+message.manager.coiot.multicast-not-supported = Device doesn't support CoIoT Multicast updates.<br/>Make sure to setup openHAB as CoIoT Peer Address ({0}).
+message.manager.coiot.mode-not-suppored = Device doesn't support request CoIoT Mode ({0}), check product documentation.
+message.manager.coiot.current-peer = CoIoT Peer Address is currently set to {0}.
+message.manager.coiot.new-peer = CoIoT mode/address will be set to {0}.
+message.manager.coiot.mode-mcast = The device starts sending CoIoT updates using IP Multicast.<br/>Please make sure that your network setup supports Multicast routing when devices are on different IP subnets.
+message.manager.coiot.mode-peer = The device will no longer send IP Multicast CoIoT updates to the network, just to the openHAB host.
+
+message.manager.fwupdate.initiated = Firmware update initiated, device returned status {0}
+message.manager.fwupdate.confirm = Do not power-off or restart device while updating the firmware!
+message.manager.fwupdate.info = Update firmware (deviceType={0}, version={1}, URL={2})
+message.manager.fwupdate.failed = Firmware updated failed: {0}
+message.manager.fwupdate.notfound = Unable to find firmware for device type {0}, version={1} (URL={2})
+message.manager.fwupdate.nofile = No firmware files found for device type {0}
+message.manager.fwupdate.success = Firmware successfully loaded - size={0}, ETag={1}, last modified={2}
# General messages
message.versioncheck.failed = Firmware-Version konnte nicht geprüft werden: {0}
-message.versioncheck.beta = Es wurde eine Betaversion erkannt: {0}/{1} ({2}), bitte sicherstellen, dass diese neuer ist als Version {3} (Release Build).
-message.versioncheck.tooold = ACHTUNG: Eine alte Firmware wurde erkannt: {0}/{1} ({2}), minimal erforderlich {3}.
+message.versioncheck.beta = Es wurde eine Betaversion erkannt: {0}/{1}.
+message.versioncheck.tooold = ACHTUNG: Eine alte Firmware wurde erkannt: {0}/{1}, minimal erforderlich {2}.
message.versioncheck.update = INFO: Eine neue Firmwareversion ist verfügbar, aktuell: {0}, neu: {1}
message.versioncheck.autocoiot = INFO: Die Firmware unterstützt die Anforderung, Auto-CoIoT wurde aktiviert.
message.init.noipaddress = Es konnte keine lokale IP-Adresse ermittelt werden. Bitte sicherstellen, dass IPv4 aktiviert ist und das richtige Interface in der openHAB Netzwerk-Konfiguration ausgewählt ist.
--- /dev/null
+ <div class="page">
+ <p class="caption">Device Action: <td>${action}</p>
+ <hr/>
+ <p/>
+ <table>
+ <tr><td>Device</td><td>${thingLabel} (${serviceName})</td></tr>
+ <tr><td>Device IP</td><td>${deviceIp} (WiFi ${wifiNetwork})</td></tr>
+ <tr><td><p/></td></tr>
+ </table>
+
+ <p class="top-distance20">
+ <span class="message">${message}</span>
+ <span id="actionMessage" class="message">Please wait ${refreshTimer}s while status is updated.</span>
+ </p>
+
+ <p style="padding-left: 6px;">
+ <button id="actionButton" class="button" onclick="location = '${actionUrl}'">${actionButtonLabel}</button>
+ </p>
+
+ <script type="text/JavaScript">
+ var refreshTimer = ${refreshTimer};
+ if (refreshTimer > 0) {
+ setTimeout("location.href = '${uri}/overview';",(refreshTimer+1)*1000);
+ document.getElementById("actionButton").style.visibility="hidden";
+ } else {
+ document.getElementById("actionMessage").style.visibility="hidden";
+ }
+ </script>
+ </div>
+ <p/>
--- /dev/null
+ </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>
--- /dev/null
+ <script type="text/JavaScript">
+ setTimeout("location.href = '${forwardLink}';",${forwardTimer});
+ </script>
--- /dev/null
+<div class="page">
+ <p class="caption">Firmware update</p>
+ <hr/>
+ <p/>
+ <table style="padding-right: 6px;">
+ <tr><td>Device</td><td>${serviceName}</td></tr>
+ <tr><td>Device Type</td><td>${modelId}</td></tr>
+ <tr><td>Device Mode</td><td>${deviceMode}</td></tr>
+ <tr><td>Device IP</td><td><a href="http://${deviceIp}" title="${deviceName}"target="_blank">${deviceIp}</a></td></tr>
+ <tr><td style="padding-left: 10px">Device Hardware Rev</td><td>${deviceHwRev}</td></tr>
+ <tr><td>WiFi</td><td>${wifiNetwork}</td></tr>
+ <tr><td><p/></td></tr>
+ <tr><td>Current firmware</td><td>${firmwareVersion}</td></tr>
+ <tr><td>Requested version</td><td>${version}</td></tr>
+ </table>
+ <p/>
+
+ <form style="padding: 6px 6px;">
+ <p>Please select connection type:</p>
+ <input type="radio" name="connection" id="internet" value="internet"/>
+ <label for="internet">Device downloads firmware directly from Internet</label><br/>
+ <input type="radio" name="connection" id="local" value="local"/>
+ <label for="local">Use openHAB as proxy (device doesn't requires Internet access)</label><br/>
+ <input type="radio" name="connection" id="custom" value="custom"/>
+ <label for="custom">Use custom URL</label>
+ <div style="padding-left: 38px;">
+ <input type="text" name = "url" id="url" value="http://" style=" padding: 3px 3px;/>
+ <label for="url"> </label>
+ </div>
+
+ <input type="hidden" id="uid" name="uid" value="${uid}">
+ <input type="hidden" id="version" name="version" value="${version}">
+ <input type="hidden" id="update" name="update" value="yes">
+
+ <p class="top-distance20">
+ <button class="button">Perform Update</button>
+ <button type="button" class="buttonCancel" onclick="location = '${uri}/overview'">Abort</button>
+ </p>
+ <p/>
+ </form>
+ <script type="text/JavaScript">
+ const connection = "${connection}";
+ document.getElementById(connection.valueOf() === "" ? "internet" : "${connection}").checked = true;
+ </script>
+ <p/>
+
+ <p class="info">${message}</p>
+</div>
+<p/>
+
--- /dev/null
+ <div class="page">
+ <p class="caption">Firmware Update</p>
+ <hr/><p/>
+ <p class="message">Updating device ${deviceName} (${uid}) with version ${version}, connection type=${connection}</p>
+ <p class="message" sytle>Update url: ${updateUrl}</p>
+
+ <span class="top-distance20">
+ <p class="info">Wait 1-2 minutes, then check device UI at <a href="http://${deviceIp}" title="${thingName}" target="_blank">${deviceIp}</a>, section Firmware.</p>
+ <p class="warning">Do not power-off or restart device while updating the firmware!</p>
+ </span>
+
+ <p>
+ <button class="button" onclick="location = '${uri}/overview'">Ok</button>
+ </p>
+ </div>
+ <p/>
+
\ No newline at end of file
--- /dev/null
+<!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>
--- /dev/null
+ <tr >
+ <td>
+ <div class="tooltip">
+ <div>
+ <img src="${uri}/images/status_${iconStatus}.png" class="statusIcon"/>
+ </div>
+ <div class="tooltiptext">
+ <table style="text-align:left; ">
+ <tr><td colspan = "2" class="caption2">${thingName}</td></tr>
+ <tr><td>Status</td><td>${thingStatus} ${thingStatusDetail}</td></tr>
+ <tr><td>CoIoT Status</td><td>${coiotStatus}</td></tr>
+ <tr><td>CoIoT Destination</td><td>${coiotDestination}</td></tr>
+ <tr><td>Cloud Status</td><td>${cloudStatus}</td></tr>
+ <tr><td>MQTT Status</td><td>${mqttStatus}</td></tr>
+ <tr><td>Actions skipped</td><td>${actionsSkipped}</td></tr>
+ <tr><td>Max Internal Temp</td><td>${maxInternalTemp}</td></tr>
+ <tr><td> <br/></td></tr>
+ ${deviceStatus}
+ </table>
+ </div>
+ </div>
+ </td>
+ <td>
+ <div class="tooltip" style="border: 1px; border-color: white;">
+ <span style="white-space: nowrap;">${displayName}</span>
+ <div class="tooltiptext">
+ <table style="text-align:left; ">
+ <tr><td colspan = "2" class="caption2">${thingName}</td></tr>
+ <tr><td>Shelly Device Name</td><td>${deviceName}</td></tr>
+ <tr><td>Device Hardware Rev</td><td>${deviceHwRev}</td></tr>
+ <tr><td>Device Type</td><td>${modelId}</td></tr>
+ <tr><td>Device Mode</td><td>${deviceMode}</td></tr>
+ <tr><td>Firmware Version</td><td>${firmwareVersion}</td></tr>
+ <tr><td>Network Name</td><td>${serviceName}</td></tr>
+ <tr><td>MAC Address</td><td>${macAddress}</td></tr>
+ <tr><td>Discoverable</td><td>${discoverable}</td></tr>
+ <tr><td>WiFi Auto Recovery</td><td>${wifiAutoRecovery}</td></tr>
+ <tr><td>WiFi AP Roaming</td><td>${apRoamingMode}</td></tr>
+ <tr><td>WiFi AP Threshold</td><td>${apRoamingThreshold}</td></tr>
+ <tr><td>Timezone</td><td>${deviceTimezone}</td></tr>
+ <tr><td>Time Server</td><td>${sntpServer}</td></tr>
+ <tr><td>Debug Mode</td><td>${debugMode}</td></tr>
+ </table>
+ </div>
+ </div>
+ </td>
+ <td>
+ <div title = "Cloud ${cloudStatus}">
+ <img src="${uri}/images/cloud_${cloudStatus}.png" class="icon"/>
+ </div>
+ </td>
+ <td>
+ <div title = "MQTT ${mqttStatus}">
+ <img src="${uri}/images/mqtt_${mqttStatus}.png" class="icon"/>
+ </div>
+ </td>
+ <td>
+ <div title="Refresh Device Status">
+ <a href="${uri}/overview?action=refresh&uid=${uid}">
+ <img src="${uri}/images/refresh.png" class="icon"/>
+ </a>
+ </div>
+ </td>
+ <td>
+ <div title="Reset Device Statistic">
+ <a href="${uri}/action?action=reset_stat&uid=${uid}">
+ <img src="${uri}/images/resetstat.png" class="icon"/>
+ </a>
+ </div>
+ </td>
+ <td>
+ <div title="Check for new firmware">
+ <a href="${uri}/action?action=otacheck&uid=${uid}">
+ <img src="${uri}/images/otacheck.png" class="icon"/>
+ </a>
+ </div>
+ </td>
+ <td><a href="http://${deviceIp}" title="${displayName}" target="_blank">${deviceIp}</a></td>
+ <td>${wifiNetwork}</td>
+ <td >
+ <div title = "Cloud ${cloudStatus}">
+ <img src="${uri}/images/wifi${wifiSignal}.png" class="icon" alt="Signal quality: ${wifiSignal} (4=best..1=very weak)"/>
+ </div>
+ </td>
+ <td align="right" nowrap> ${wifiSignalRssi}</td>
+ <td align="center" nowrap>${batteryLevel}</td>
+ <td>${heartBeat}</td>
+ <td>${actionList}</td>
+ <td nowrap>${firmwareVersion}</td>
+ <td align="center">${updateAvailable}</td>
+ <td>${firmwareSelection}</td>
+ <td align="right" nowrap>${uptime}</td>
+ <td align="center" nowrap title="Max Internal Device Temp so far: ${maxInternalTemp}">${internalTemp}</td>
+ <td align="right" nowrap>${devUpdatePeriod} s</td>
+ <td align="right">${remainingWatchdog} s</td>
+ <td align="right">${alarmCount}</td>
+ <td nowrap>
+ <a href="${uri}/action?action=reset_stat&uid=${uid}" title="Clear alarm">${lastAlarm}</a>
+ </td>
+ <td>${lastAlarmTs}</td>
+ <td align="right">${deviceRestarts}</td>
+ <td align="right">${timeoutErrors}</td>
+ <td align="right">${timeoutsRecovered}</td>
+ <td align="right" title="CoIoT Status: ${coiotStatus}">${coiotMessages}</td>
+ <td align="right">${coiotErrors}</td>
+ </tr>
--- /dev/null
+ <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>
--- /dev/null
+ <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>
--- /dev/null
+ <div class="navigation">
+ <table><tr><td>
+ Device Filter
+ <select name="thingFilter" id="thingFilter" onchange="location = '${uri}/overview?filter='+this.options[this.selectedIndex].value;">
+ <option value="" selected disabled>select</option>
+ <option value="*">All</option>
+ <option value="online">Online only</option>
+ <option value="inactive">Inactive only</option>
+ <option value="attention">Needs Attention</option>
+ <option value="update">Update available</option>
+ <option value="unprotected">Unprotected</option>
+ </select>
+ <a href="${uri}/overview?action=refresh">
+ <img src="${uri}/images/refresh.png" class="navRefIcon" title="Refresh Status for all Devices"/>
+ </a>
+ <a href="${uri}/overview?action=reset_stat">
+ <img src="${uri}/images/resetstat.png" class="navRefIcon" title="Reset Statistics for all Devices"/>
+ </a>
+ <a href="${uri}/overview?action=otacheck">
+ <img src="${uri}/images/otacheck.png" class="navRefIcon" title="Trigger Firmware Check for all Devices"/>
+ </a>
+ </td></tr></table>
+ </div>
+
+ <div class="overview">
+ <hr/>
+ <br/>
+
+ <table class="devTable">
+ <tbody>
+ <tr>
+ <th>S</th>
+ <th align="left">Name</th>
+ <th> </th>
+ <th> </th>
+ <th> </th>
+ <th> </th>
+ <th> </th>
+ <th align="left">Device IP</th>
+ <th align="left">WiFi Network</th>
+ <th colspan = "2">WiFi Signal</th>
+ <th>Battery Level</th>
+ <th align="left">Heartbeat</th>
+ <th>Actions</th>
+ <th align="left">Firmware</th>
+ <th>Update avail</th>
+ <th align="left">Versions</th>
+ <th>Uptime</th>
+ <th>Internal Temp</th>
+ <th>Update Period</th>
+ <th>Remaining Watchdog</th>
+ <th>Events</th>
+ <th>Last Event</th>
+ <th>Event Time</th>
+ <th>Device Restarts</th>
+ <th>Timeout Errors</th>
+ <th>Timeouts Recovered</th>
+ <th>CoIOT Messages</th>
+ <th>CoIOT Errors</th>
+ </tr>