[shelly] New Shelly Manager (more detailed information, status, integrated firmware upgrade) (#10276)

* This PR brings in the Shelly Manager, check doc/ShellyManager.md for
additional information.
* Restart Device in Manager when CoIoT Mode has changed
* Updated pattern to extract version info (thanks @fwolter), CoIoT warning
for non-Motion devices fixed; AdvancedUsers.md now refers to Shelly
Manager
* Modified message when beta is detected (reference to 1.5.7 release build
confuses users running 1.10 beta)
* Fix for Enable/Disable AP roaming
* Handle button events also in detached mode, README updated
* Ignore inconsistent version string for initial 1.10 releases
* removed display of firmware id (there are various formats and it has
no value)

Signed-off-by: Markus Michels <markus7017@gmail.com>
This commit is contained in:
Markus Michels
2021-03-31 22:42:33 +02:00
committed by GitHub
parent b9d3c35732
commit 1783017be4
55 changed files with 2790 additions and 38 deletions

View File

@@ -194,7 +194,6 @@ public class ShellyBindingConstants {
public static final String PROPERTY_STATS_TIMEOUTS = "statsTimeoutErrors";
public static final String PROPERTY_STATS_TRECOVERED = "statsTimeoutsRecovered";
public static final String PROPERTY_COIOTAUTO = "coiotAutoEnable";
public static final String PROPERTY_COIOTREFRESH = "coiotAutoRefresh";
// Relay
public static final String CHANNEL_GROUP_RELAY_CONTROL = "relay";
@@ -327,6 +326,7 @@ public class ShellyBindingConstants {
public static final String SHELLY_API_MIN_FWVERSION = "v1.5.7";// v1.5.7+
public static final String SHELLY_API_MIN_FWCOIOT = "v1.6";// v1.6.0+
public static final String SHELLY_API_FWCOIOT2 = "v1.8";// CoAP 2 with FW 1.8+
public static final String SHELLY_API_FW_110 = "v1.10"; // FW 1.10 or newer detected, activates some add feature
// Alarm types/messages
public static final String ALARM_TYPE_NONE = "NONE";

View File

@@ -543,7 +543,9 @@ public class ShellyApiJsonDTO {
@SerializedName("wifi_sta1")
public ShellySettingsWiFiNetwork wifiSta1;
@SerializedName("wifirecovery_reboot_enabled")
public Boolean wifiRecoveryReboot;
public Boolean wifiRecoveryReboot; // FW 1.10+
@SerializedName("ap_roaming")
public ShellyApRoaming apRoaming; // FW 1.10+
public ShellySettingsMqtt mqtt; // not used for now
public ShellySettingsSntp sntp; // not used for now
@@ -563,6 +565,7 @@ public class ShellyApiJsonDTO {
public ShellySensorSleepMode sleepMode; // FW 1.6
@SerializedName("external_power")
public Integer externalPower; // H&T FW 1.6, seems to be the same like charger for the Sense
public Boolean debug_enable; // FW 1.10+
public String timezone;
public Double lat;
@@ -891,6 +894,15 @@ public class ShellyApiJsonDTO {
public Integer currentPos; // current position 0..100, 100=open
}
public class ShellyOtaCheckResult {
public String status;
}
public class ShellyApRoaming {
public Boolean enabled;
public Integer threshold;
}
public class ShellySensorSleepMode {
public Integer period;
public String unit;

View File

@@ -63,6 +63,10 @@ public class ShellyApiResult {
return httpCode == OK_200;
}
public boolean isNotFound() {
return httpCode == NOT_FOUND_404;
}
public boolean isHttpAccessUnauthorized() {
return (httpCode == UNAUTHORIZED_401 || response.contains(SHELLY_APIERR_UNAUTHORIZED));
}

View File

@@ -29,6 +29,7 @@ import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsIn
import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsRelay;
import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsRgbwLight;
import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsStatus;
import org.openhab.binding.shelly.internal.util.ShellyVersionDTO;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -44,12 +45,13 @@ import com.google.gson.Gson;
@NonNullByDefault
public class ShellyDeviceProfile {
private final Logger logger = LoggerFactory.getLogger(ShellyDeviceProfile.class);
private final static Pattern VERSION_PATTERN = Pattern.compile("v\\d+\\.\\d+\\.\\d+");
private final static Pattern VERSION_PATTERN = Pattern.compile("v\\d+\\.\\d+\\.\\d+(-[a-z0-9]*)?");
public boolean initialized = false; // true when initialized
public String thingName = "";
public String deviceType = "";
public boolean extFeatures = false;
public String settingsJson = "";
public ShellySettingsGlobal settings = new ShellySettingsGlobal();
@@ -64,7 +66,6 @@ public class ShellyDeviceProfile {
public String hwRev = "";
public String hwBatchId = "";
public String mac = "";
public String fwId = "";
public String fwVersion = "";
public String fwDate = "";
@@ -126,7 +127,8 @@ public class ShellyDeviceProfile {
hwBatchId = settings.hwinfo != null ? getString(settings.hwinfo.batchId.toString()) : "";
fwDate = substringBefore(settings.fw, "/");
fwVersion = extractFwVersion(settings.fw);
fwId = substringAfter(settings.fw, "@");
ShellyVersionDTO version = new ShellyVersionDTO();
extFeatures = version.compare(fwVersion, SHELLY_API_FW_110) >= 0;
discoverable = (settings.discoverable == null) || settings.discoverable;
inColor = isLight && mode.equalsIgnoreCase(SHELLY_MODE_COLOR);
@@ -314,7 +316,8 @@ public class ShellyDeviceProfile {
logger.trace("{}: Checking for trigger, button-type[{}] is {}", thingName, idx, btnType);
return btnType.equalsIgnoreCase(SHELLY_BTNT_MOMENTARY) || btnType.equalsIgnoreCase(SHELLY_BTNT_MOM_ON_RELEASE)
|| btnType.equalsIgnoreCase(SHELLY_BTNT_ONE_BUTTON) || btnType.equalsIgnoreCase(SHELLY_BTNT_TWO_BUTTON);
|| btnType.equalsIgnoreCase(SHELLY_BTNT_ONE_BUTTON) || btnType.equalsIgnoreCase(SHELLY_BTNT_TWO_BUTTON)
|| btnType.equalsIgnoreCase(SHELLY_BTNT_DETACHED);
}
public int getRollerFav(int id) {
@@ -327,9 +330,12 @@ public class ShellyDeviceProfile {
public static String extractFwVersion(@Nullable String version) {
if (version != null) {
Matcher matcher = VERSION_PATTERN.matcher(version);
// fix version e.g. 20210319-122304/v.1.10-Dimmer1-gfd4cc10 (with v.1. instead of v1.)
String vers = version.replace("/v.1.10-", "/v1.10.0-");
// Extract version from string, e.g. 20210226-091047/v1.10.0-rc2-89-g623b41ec0-master
Matcher matcher = VERSION_PATTERN.matcher(vers);
if (matcher.find()) {
// e.g. 20210226-091047/v1.10.0-rc2-89-g623b41ec0-master
return matcher.group(0);
}
}

View File

@@ -32,6 +32,7 @@ import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.HttpStatus;
import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellyControlRoller;
import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellyOtaCheckResult;
import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySendKeyList;
import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySenseKeyCode;
import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsDevice;
@@ -90,6 +91,14 @@ public class ShellyHttpApi {
return callApi(SHELLY_URL_DEVINFO, ShellySettingsDevice.class);
}
public String setDebug(boolean enabled) throws ShellyApiException {
return callApi(SHELLY_URL_SETTINGS + "?debug_enable=" + Boolean.valueOf(enabled), String.class);
}
public String getDebugLog(String id) throws ShellyApiException {
return callApi("/debug/" + id, String.class);
}
/**
* Initialize the device profile
*
@@ -241,6 +250,17 @@ public class ShellyHttpApi {
ShellySettingsLogin.class);
}
public String getCoIoTDescription() throws ShellyApiException {
try {
return callApi("/cit/d", String.class);
} catch (ShellyApiException e) {
if (e.getApiResult().isNotFound()) {
return ""; // only supported by FW 1.10+
}
throw e;
}
}
public ShellySettingsLogin setCoIoTPeer(String peer) throws ShellyApiException {
return callApi(SHELLY_URL_SETTINGS + "?coiot_enable=true&coiot_peer=" + peer, ShellySettingsLogin.class);
}
@@ -253,6 +273,23 @@ public class ShellyHttpApi {
return callApi(SHELLY_URL_SETTINGS + "?reset=true", String.class);
}
public ShellyOtaCheckResult checkForUpdate() throws ShellyApiException {
return callApi("/ota/check", ShellyOtaCheckResult.class); // nw FW 1.10+: trigger update check
}
public String setWiFiRecovery(boolean enable) throws ShellyApiException {
return callApi(SHELLY_URL_SETTINGS + "?wifirecovery_reboot_enabled=" + (enable ? "true" : "false"),
String.class); // FW 1.10+: Enable auto-restart on WiFi problems
}
public String setApRoaming(boolean enable) throws ShellyApiException { // FW 1.10+: Enable AP Roadming
return callApi(SHELLY_URL_SETTINGS + "?ap_roaming_enabled=" + (enable ? "true" : "false"), String.class);
}
public String resetStaCache() throws ShellyApiException { // FW 1.10+: Reset cached STA/AP list and to a rescan
return callApi("/sta_cache_reset", String.class);
}
public ShellySettingsUpdate firmwareUpdate(String uri) throws ShellyApiException {
return callApi("/ota?" + uri, ShellySettingsUpdate.class);
}
@@ -560,7 +597,8 @@ public class ShellyHttpApi {
if (contentResponse.getStatus() != HttpStatus.OK_200) {
throw new ShellyApiException(apiResult);
}
if (response.isEmpty() || !response.startsWith("{") && !response.startsWith("[")) {
if (response.isEmpty() || !response.startsWith("{") && !response.startsWith("[") && !url.contains("/debug/")
&& !url.contains("/sta_cache_reset")) {
throw new ShellyApiException("Unexpected response: " + response);
}
} catch (ExecutionException | InterruptedException | TimeoutException | IllegalArgumentException e) {

View File

@@ -22,6 +22,7 @@ import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.shelly.internal.api.ShellyDeviceProfile;
import org.openhab.binding.shelly.internal.api.ShellyHttpApi;
import org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO.CoIotDescrBlk;
import org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO.CoIotDescrSen;
import org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO.CoIotSensor;
@@ -50,6 +51,7 @@ public class ShellyCoIoTProtocol {
protected final String thingName;
protected final ShellyBaseHandler thingHandler;
protected final ShellyDeviceProfile profile;
protected final ShellyHttpApi api;
protected final Map<String, CoIotDescrBlk> blkMap;
protected final Map<String, CoIotDescrSen> sensorMap;
private final Gson gson = new GsonBuilder().create();
@@ -68,6 +70,7 @@ public class ShellyCoIoTProtocol {
this.blkMap = blkMap;
this.sensorMap = sensorMap;
this.profile = thingHandler.getProfile();
this.api = thingHandler.getApi();
}
protected boolean handleStatusUpdate(List<CoIotSensor> sensorUpdates, CoIotDescrSen sen, CoIotSensor s,

View File

@@ -37,6 +37,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.shelly.internal.api.ShellyApiException;
import org.openhab.binding.shelly.internal.api.ShellyDeviceProfile;
import org.openhab.binding.shelly.internal.api.ShellyHttpApi;
import org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO.CoIotDescrBlk;
import org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO.CoIotDescrSen;
import org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO.CoIotDevDescrTypeAdapter;
@@ -80,7 +81,7 @@ public class ShellyCoapHandler implements ShellyCoapListener {
private @Nullable CoapClient statusClient;
private Request reqDescription = new Request(Code.GET, Type.CON);
private Request reqStatus = new Request(Code.GET, Type.CON);
private boolean discovering = false;
private boolean updatesRequested = false;
private int coiotPort = COIOT_PORT;
private long coiotMessages = 0;
@@ -90,11 +91,13 @@ public class ShellyCoapHandler implements ShellyCoapListener {
private Map<String, CoIotDescrBlk> blkMap = new LinkedHashMap<>();
private Map<String, CoIotDescrSen> sensorMap = new LinkedHashMap<>();
private ShellyDeviceProfile profile;
private ShellyHttpApi api;
public ShellyCoapHandler(ShellyBaseHandler thingHandler, ShellyCoapServer coapServer) {
this.thingHandler = thingHandler;
this.thingName = thingHandler.thingName;
this.profile = thingHandler.getProfile();
this.api = thingHandler.getApi();
this.coapServer = coapServer;
this.coiot = new ShellyCoIoTVersion2(thingName, thingHandler, blkMap, sensorMap); // Default: V2
@@ -137,6 +140,7 @@ public class ShellyCoapHandler implements ShellyCoapListener {
logger.warn("{}: Unable to initialize CoAP access (network error)", thingName);
throw new ShellyApiException("Network initialization failed");
}
discover();
} catch (SocketException e) {
logger.warn("{}: Unable to initialize CoAP access (socket exception) - {}", thingName, e.getMessage());
@@ -283,10 +287,10 @@ public class ShellyCoapHandler implements ShellyCoapListener {
coiotErrors++;
}
if (!discovering) {
if (!updatesRequested) {
// Observe Status Updates
reqStatus = sendRequest(reqStatus, config.deviceIp, COLOIT_URI_DEVSTATUS, Type.NON);
discovering = true;
updatesRequested = true;
}
} catch (JsonSyntaxException | IllegalArgumentException | NullPointerException e) {
logger.debug("{}: Unable to process CoIoT Message for payload={}", thingName, payload, e);
@@ -538,6 +542,21 @@ public class ShellyCoapHandler implements ShellyCoapListener {
}
private void discover() {
if (coiot.getVersion() >= 2) {
{
try {
// Try to device description using http request (FW 1.10+)
String payload = api.getCoIoTDescription();
if (!payload.isEmpty()) {
logger.debug("{}: Using CoAP device description from successful HTTP /cit/d", thingName);
handleDeviceDescription(thingName, payload);
return;
}
} catch (ShellyApiException e) {
// ignore if not supported by device
}
}
}
reqDescription = sendRequest(reqDescription, config.deviceIp, COLOIT_URI_DEVDESC, Type.CON);
}

View File

@@ -32,6 +32,7 @@ import org.eclipse.jetty.client.HttpClient;
import org.openhab.binding.shelly.internal.api.ShellyApiException;
import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO;
import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellyInputState;
import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellyOtaCheckResult;
import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsDevice;
import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsStatus;
import org.openhab.binding.shelly.internal.api.ShellyApiResult;
@@ -245,9 +246,8 @@ public class ShellyBaseHandler extends BaseThingHandler implements ShellyDeviceL
}
tmpPrf.auth = devInfo.auth; // missing in /settings
logger.debug("{}: Initializing device {}, type {}, Hardware: Rev: {}, batch {}; Firmware: {} / {} ({})",
thingName, tmpPrf.hostname, tmpPrf.deviceType, tmpPrf.hwRev, tmpPrf.hwBatchId, tmpPrf.fwVersion,
tmpPrf.fwDate, tmpPrf.fwId);
logger.debug("{}: Initializing device {}, type {}, Hardware: Rev: {}, batch {}; Firmware: {} / {}", thingName,
tmpPrf.hostname, tmpPrf.deviceType, tmpPrf.hwRev, tmpPrf.hwBatchId, tmpPrf.fwVersion, tmpPrf.fwDate);
logger.debug("{}: Shelly settings info for {}: {}", thingName, tmpPrf.hostname, tmpPrf.settingsJson);
logger.debug("{}: Device "
+ "hasRelays:{} (numRelays={}),isRoller:{} (numRoller={}),isDimmer:{},numMeter={},isEMeter:{})"
@@ -273,7 +273,7 @@ public class ShellyBaseHandler extends BaseThingHandler implements ShellyDeviceL
} catch (ShellyApiException e) {
logger.debug("{}: Unable to set CoIoT peer: {}", thingName, e.toString());
}
} else if (!devpeer.equals(ourpeer)) {
} else if (!devpeer.isEmpty() && !devpeer.equals(ourpeer)) {
logger.warn("{}: CoIoT peer in device settings does not point this to this host, disabling CoIoT",
thingName);
config.eventsCoIoT = autoCoIoT = false;
@@ -396,9 +396,15 @@ public class ShellyBaseHandler extends BaseThingHandler implements ShellyDeviceL
// Get profile, if refreshSettings == true reload settings from device
logger.trace("{}: Updating status (refreshSettings={})", thingName, refreshSettings);
ShellySettingsStatus status = api.getStatus();
profile = getProfile(refreshSettings || checkRestarted(status));
boolean restarted = checkRestarted(status);
profile = getProfile(refreshSettings || restarted);
profile.status = status;
profile.updateFromStatus(status);
if (restarted) {
logger.debug("{}: Device restart #{} detected", thingName, stats.restarts);
stats.restarts++;
postEvent(ALARM_TYPE_RESTARTED, true);
}
// If status update was successful the thing must be online
setThingOnline();
@@ -571,9 +577,6 @@ public class ShellyBaseHandler extends BaseThingHandler implements ShellyDeviceL
private boolean checkRestarted(ShellySettingsStatus status) {
if (profile.isInitialized() && (status.uptime < stats.lastUptime || !profile.status.update.oldVersion.isEmpty()
&& !status.update.oldVersion.equals(profile.status.update.oldVersion))) {
logger.debug("{}: Device restart #{} detected", thingName, stats.restarts);
stats.restarts++;
postEvent(ALARM_TYPE_RESTARTED, true);
updateProperties(profile, status);
return true;
}
@@ -799,12 +802,11 @@ public class ShellyBaseHandler extends BaseThingHandler implements ShellyDeviceL
try {
ShellyVersionDTO version = new ShellyVersionDTO();
if (version.checkBeta(getString(prf.fwVersion))) {
logger.info("{}: {}", prf.hostname, messages.get("versioncheck.beta", prf.fwVersion, prf.fwDate,
prf.fwId, SHELLY_API_MIN_FWVERSION));
logger.info("{}: {}", prf.hostname, messages.get("versioncheck.beta", prf.fwVersion, prf.fwDate));
} else {
if ((version.compare(prf.fwVersion, SHELLY_API_MIN_FWVERSION) < 0) && !profile.isMotion) {
logger.warn("{}: {}", prf.hostname, messages.get("versioncheck.tooold", prf.fwVersion, prf.fwDate,
prf.fwId, SHELLY_API_MIN_FWVERSION));
logger.warn("{}: {}", prf.hostname,
messages.get("versioncheck.tooold", prf.fwVersion, prf.fwDate, SHELLY_API_MIN_FWVERSION));
}
}
if (bindingConfig.autoCoIoT && ((version.compare(prf.fwVersion, SHELLY_API_MIN_FWCOIOT)) >= 0)
@@ -1081,7 +1083,6 @@ public class ShellyBaseHandler extends BaseThingHandler implements ShellyDeviceL
properties.put(PROPERTY_UPDATE_NEW_VERS, getString(status.update.newVersion));
}
properties.put(PROPERTY_COIOTAUTO, String.valueOf(autoCoIoT));
properties.put(PROPERTY_COIOTREFRESH, String.valueOf(autoCoIoT));
Map<String, String> thingProperties = new TreeMap<>();
for (Map.Entry<String, Object> property : properties.entrySet()) {
@@ -1142,8 +1143,7 @@ public class ShellyBaseHandler extends BaseThingHandler implements ShellyDeviceL
if (profile.isInitialized()) {
properties.put(PROPERTY_MODEL_ID, getString(profile.settings.device.type));
properties.put(PROPERTY_MAC_ADDRESS, profile.mac);
properties.put(PROPERTY_FIRMWARE_VERSION,
profile.fwVersion + "/" + profile.fwDate + "(" + profile.fwId + ")");
properties.put(PROPERTY_FIRMWARE_VERSION, profile.fwVersion + "/" + profile.fwDate);
properties.put(PROPERTY_DEV_MODE, profile.mode);
properties.put(PROPERTY_NUM_RELAYS, String.valueOf(profile.numRelays));
properties.put(PROPERTY_NUM_ROLLERS, String.valueOf(profile.numRollers));
@@ -1264,4 +1264,13 @@ public class ShellyBaseHandler extends BaseThingHandler implements ShellyDeviceL
public Map<String, String> getStatsProp() {
return stats.asProperties();
}
public String checkForUpdate() {
try {
ShellyOtaCheckResult result = api.checkForUpdate();
return result.status;
} catch (ShellyApiException e) {
return "";
}
}
}

View File

@@ -0,0 +1,65 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.shelly.internal.manager;
import static org.openhab.binding.shelly.internal.manager.ShellyManagerConstants.*;
import java.util.LinkedHashMap;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.http.HttpStatus;
import org.openhab.binding.shelly.internal.ShellyHandlerFactory;
import org.openhab.binding.shelly.internal.api.ShellyApiException;
import org.openhab.binding.shelly.internal.manager.ShellyManagerPage.ShellyMgrResponse;
import org.openhab.binding.shelly.internal.provider.ShellyTranslationProvider;
import org.osgi.service.cm.ConfigurationAdmin;
/**
* {@link ShellyManager} implements the Shelly Manager
*
* @author Markus Michels - Initial contribution
*/
@NonNullByDefault
public class ShellyManager {
private final Map<String, ShellyManagerPage> pages = new LinkedHashMap<>();
private final ShellyHandlerFactory handlerFactory;
public ShellyManager(ConfigurationAdmin configurationAdmin, ShellyTranslationProvider translationProvider,
HttpClient httpClient, String localIp, int localPort, ShellyHandlerFactory handlerFactory) {
this.handlerFactory = handlerFactory;
pages.put(SHELLY_MGR_OVERVIEW_URI, new ShellyManagerOverviewPage(configurationAdmin, translationProvider,
httpClient, localIp, localPort, handlerFactory));
pages.put(SHELLY_MGR_ACTION_URI, new ShellyManagerActionPage(configurationAdmin, translationProvider,
httpClient, localIp, localPort, handlerFactory));
pages.put(SHELLY_MGR_FWUPDATE_URI, new ShellyManagerOtaPage(configurationAdmin, translationProvider, httpClient,
localIp, localPort, handlerFactory));
pages.put(SHELLY_MGR_OTA_URI, new ShellyManagerOtaPage(configurationAdmin, translationProvider, httpClient,
localIp, localPort, handlerFactory));
pages.put(SHELLY_MGR_IMAGES_URI, new ShellyManagerImageLoader(configurationAdmin, translationProvider,
httpClient, localIp, localPort, handlerFactory));
pages.put(SHELLY_MANAGER_URI, new ShellyManagerOverviewPage(configurationAdmin, translationProvider, httpClient,
localIp, localPort, handlerFactory));
}
public ShellyMgrResponse generateContent(String path, Map<String, String[]> parameters) throws ShellyApiException {
for (Map.Entry<String, ShellyManagerPage> page : pages.entrySet()) {
if (path.toLowerCase().startsWith(page.getKey())) {
ShellyManagerPage p = page.getValue();
return p.generateContent(path, parameters);
}
}
return new ShellyMgrResponse("Invalid URL or syntax", HttpStatus.BAD_REQUEST_400);
}
}

View File

@@ -0,0 +1,356 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.shelly.internal.manager;
import static org.openhab.binding.shelly.internal.ShellyBindingConstants.PROPERTY_SERVICE_NAME;
import static org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.SHELLY_COIOT_MCAST;
import static org.openhab.binding.shelly.internal.manager.ShellyManagerConstants.*;
import static org.openhab.binding.shelly.internal.util.ShellyUtils.*;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.http.HttpStatus;
import org.openhab.binding.shelly.internal.ShellyHandlerFactory;
import org.openhab.binding.shelly.internal.api.ShellyApiException;
import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellyOtaCheckResult;
import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsLogin;
import org.openhab.binding.shelly.internal.api.ShellyDeviceProfile;
import org.openhab.binding.shelly.internal.api.ShellyHttpApi;
import org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO;
import org.openhab.binding.shelly.internal.config.ShellyThingConfiguration;
import org.openhab.binding.shelly.internal.handler.ShellyManagerInterface;
import org.openhab.binding.shelly.internal.provider.ShellyTranslationProvider;
import org.openhab.core.thing.ThingStatusDetail;
import org.osgi.service.cm.ConfigurationAdmin;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* {@link ShellyManagerActionPage} implements the Shelly Manager's action page
*
* @author Markus Michels - Initial contribution
*/
@NonNullByDefault
public class ShellyManagerActionPage extends ShellyManagerPage {
private final Logger logger = LoggerFactory.getLogger(ShellyManagerActionPage.class);
public ShellyManagerActionPage(ConfigurationAdmin configurationAdmin, ShellyTranslationProvider translationProvider,
HttpClient httpClient, String localIp, int localPort, ShellyHandlerFactory handlerFactory) {
super(configurationAdmin, translationProvider, httpClient, localIp, localPort, handlerFactory);
}
@Override
public ShellyMgrResponse generateContent(String path, Map<String, String[]> parameters) throws ShellyApiException {
String action = getUrlParm(parameters, URLPARM_ACTION);
String uid = getUrlParm(parameters, URLPARM_UID);
String update = getUrlParm(parameters, URLPARM_UPDATE);
if (uid.isEmpty() || action.isEmpty()) {
return new ShellyMgrResponse("Invalid URL parameters: " + parameters.toString(),
HttpStatus.BAD_REQUEST_400);
}
Map<String, String> properties = new HashMap<>();
properties.put(ATTRIBUTE_METATAG, "");
properties.put(ATTRIBUTE_CSS_HEADER, "");
properties.put(ATTRIBUTE_CSS_FOOTER, "");
String html = loadHTML(HEADER_HTML, properties);
ShellyManagerInterface th = getThingHandler(uid);
if (th != null) {
fillProperties(properties, uid, th);
Map<String, String> actions = getActions(th.getProfile());
String actionUrl = SHELLY_MGR_OVERVIEW_URI;
String actionButtonLabel = "OK"; // Default
String serviceName = getValue(properties, PROPERTY_SERVICE_NAME);
String message = "";
ShellyThingConfiguration config = getThingConfig(th, properties);
ShellyDeviceProfile profile = th.getProfile();
ShellyHttpApi api = th.getApi();
new ShellyHttpApi(uid, config, httpClient);
int refreshTimer = 0;
switch (action) {
case ACTION_RES_STATS:
th.resetStats();
message = getMessageP("action.resstats.confirm", MCINFO);
refreshTimer = 3;
break;
case ACTION_RESTART:
if (update.equalsIgnoreCase("yes")) {
message = getMessageP("action.restart.info", MCINFO);
actionButtonLabel = "Ok";
new Thread(() -> { // schedule asynchronous reboot
try {
api.deviceReboot();
} catch (ShellyApiException e) {
// maybe the device restarts before returning the http response
}
setRestarted(th, uid); // refresh after reboot
}).start();
refreshTimer = profile.isMotion ? 60 : 30;
} else {
message = getMessageS("action.restart.confirm", MCINFO);
actionUrl = buildActionUrl(uid, action);
}
break;
case ACTION_PROTECT:
// Get device settings
if (config.userId.isEmpty() || config.password.isEmpty()) {
message = getMessageP("action.protect.id-missing", MCWARNING);
break;
}
if (!update.equalsIgnoreCase("yes")) {
ShellySettingsLogin status = api.getLoginSettings();
message = getMessage("action.protect.status", getBool(status.enabled) ? "enabled" : "disabled",
status.username)
+ getMessageP("action.protect.new", MCINFO, config.userId, config.password);
actionUrl = buildActionUrl(uid, action);
} else {
api.setLoginCredentials(config.userId, config.password);
message = getMessageP("action.protect.confirm", MCINFO, config.userId, config.password);
refreshTimer = 3;
}
break;
case ACTION_SETCOIOT_MCAST:
case ACTION_SETCOIOT_PEER:
if ((profile.settings.coiot == null) || (profile.settings.coiot.peer == null)) {
// feature not available
message = getMessage("coiot.mode-not-suppored", MCWARNING, action);
break;
}
String peer = getString(profile.settings.coiot.peer);
boolean mcast = peer.isEmpty() || peer.equalsIgnoreCase(SHELLY_COIOT_MCAST);
String newPeer = mcast ? localIp + ":" + ShellyCoapJSonDTO.COIOT_PORT : SHELLY_COIOT_MCAST;
String displayPeer = mcast ? newPeer : "Multicast";
if (profile.isMotion && action.equalsIgnoreCase(ACTION_SETCOIOT_MCAST)) {
// feature not available
message = getMessageP("coiot.multicast-not-supported", "warning", displayPeer);
break;
}
if (!update.equalsIgnoreCase("yes")) {
message = getMessageP("coiot.current-peer", MCMESSAGE, mcast ? "Multicast" : peer)
+ getMessageP("coiot.new-peer", MCINFO, displayPeer)
+ getMessageP(mcast ? "coiot.mode-peer" : "coiot.mode-mcast", MCMESSAGE);
actionUrl = buildActionUrl(uid, action);
} else {
new Thread(() -> { // schedule asynchronous reboot
try {
api.setCoIoTPeer(newPeer);
api.deviceReboot();
} catch (ShellyApiException e) {
// maybe the device restarts before returning the http response
}
setRestarted(th, uid); // refresh after reboot
}).start();
// The device needs a restart after changing the peer mode
message = getMessageP("action.restart.info", MCINFO);
refreshTimer = 30;
}
break;
case ACTION_ENCLOUD:
case ACTION_DISCLOUD:
boolean enabled = action.equals(ACTION_ENCLOUD);
api.setCloud(enabled);
message = getMessageP("action.setcloud.config", MCINFO, enabled ? "enabled" : "disabled");
refreshTimer = 20;
break;
case ACTION_RESET:
if (!update.equalsIgnoreCase("yes")) {
message = getMessageP("action.reset.warning", MCWARNING, serviceName);
actionUrl = buildActionUrl(uid, action);
} else {
new Thread(() -> { // schedule asynchronous reboot
try {
api.factoryReset();
setRestarted(th, uid);
} catch (ShellyApiException e) {
// maybe the device restarts before returning the http response
}
}).start();
message = getMessageP("action.reset.confirm", MCINFO, serviceName);
refreshTimer = 5;
}
break;
case ACTION_OTACHECK:
try {
ShellyOtaCheckResult result = api.checkForUpdate();
message = getMessage("action.checkupd." + result.status);
} catch (ShellyApiException e) {
// maybe the device restarts before returning the http response
message = getMessageP("action.checkupd.failed", e.toString());
}
refreshTimer = 3;
break;
case ACTION_ENDEBUG:
case ACTION_DISDEBUG:
boolean enable = action.equalsIgnoreCase(ACTION_ENDEBUG);
if (!update.equalsIgnoreCase("yes")) {
message = getMessage(enable ? "action.debug-enable" : "action.debug-disable");
actionUrl = buildActionUrl(uid, action);
} else {
new Thread(() -> { // schedule asynchronous reboot
try {
api.setDebug(enable);
} catch (ShellyApiException e) {
// maybe the device restarts before returning the http response
}
}).start();
message = getMessage("action.debug-confirm", enable ? "enabled" : "disabled");
refreshTimer = 3;
}
break;
case ACTION_RESSTA:
if (!update.equalsIgnoreCase("yes")) {
message = getMessage("action.resetsta-info");
actionUrl = buildActionUrl(uid, action);
} else {
try {
String result = api.resetStaCache();
message = getMessage("action.resetsta-confirm");
} catch (ShellyApiException e) {
message = getMessageP("action.resetsta-failed", e.toString());
}
refreshTimer = 10;
}
break;
case ACTION_ENWIFIREC:
case ACTION_DISWIFIREC:
enable = action.equalsIgnoreCase(ACTION_ENWIFIREC);
if (!update.equalsIgnoreCase("yes")) {
message = getMessage(enable ? "action.setwifirec-enable" : "action.setwifirec-disable");
actionUrl = buildActionUrl(uid, action);
} else {
try {
String result = api.setWiFiRecovery(enable);
message = getMessage("action.setwifirec-confirm", enable ? "enabled" : "disabled");
} catch (ShellyApiException e) {
message = getMessage("action.setwifirec-failed", e.toString());
}
refreshTimer = 3;
}
break;
case ACTION_ENAPROAMING:
case ACTION_DISAPROAMING:
enable = action.equalsIgnoreCase(ACTION_ENAPROAMING);
if (!update.equalsIgnoreCase("yes")) {
message = getMessage(enable ? "action.aproaming-enable" : "action.aproaming-disable");
actionUrl = buildActionUrl(uid, action);
} else {
try {
String result = api.setApRoaming(enable);
message = getMessage("action.aproaming-confirm", enable ? "enabled" : "disabled");
} catch (ShellyApiException e) {
message = getMessage("action.aproaming-failed", e.toString());
}
refreshTimer = 3;
}
break;
case ACTION_GETDEB:
case ACTION_GETDEB1:
try {
message = api.getDebugLog(action.equalsIgnoreCase(ACTION_GETDEB) ? "log" : "log1");
message = message.replaceAll("[\r]", "").replaceAll("[\r\n]", "<br>");
} catch (ShellyApiException e) {
message = getMessage("action.getdebug-failed", e.toString());
}
break;
case ACTION_NONE:
break;
default:
logger.warn("{}: {}", LOG_PREFIX, getMessage("action.unknown", action));
}
properties.put(ATTRIBUTE_ACTION, getString(actions.get(action))); // get description for command
properties.put(ATTRIBUTE_ACTION_BUTTON, actionButtonLabel);
properties.put(ATTRIBUTE_ACTION_URL, actionUrl);
message = fillAttributes(message, properties);
properties.put(ATTRIBUTE_MESSAGE, message);
properties.put(ATTRIBUTE_REFRESH, String.valueOf(refreshTimer));
html += loadHTML(ACTION_HTML, properties);
th.requestUpdates(1, refreshTimer > 0); // trigger background update
}
properties.clear();
html += loadHTML(FOOTER_HTML, properties);
return new ShellyMgrResponse(html, HttpStatus.OK_200);
}
public static Map<String, String> getActions(ShellyDeviceProfile profile) {
Map<String, String> list = new LinkedHashMap<>();
list.put(ACTION_RES_STATS, "Reset Statistics");
list.put(ACTION_RESTART, "Reboot Device");
list.put(ACTION_PROTECT, "Protect Device");
if ((profile.settings.coiot != null) && (profile.settings.coiot.peer != null) && !profile.isMotion) {
boolean mcast = profile.settings.coiot.peer.isEmpty()
|| profile.settings.coiot.peer.equalsIgnoreCase(SHELLY_COIOT_MCAST);
list.put(mcast ? ACTION_SETCOIOT_PEER : ACTION_SETCOIOT_MCAST,
mcast ? "Set CoIoT Peer Mode" : "Set CoIoT Multicast Mode");
}
if (profile.isSensor && !profile.isMotion && (profile.settings.wifiSta != null)
&& profile.settings.wifiSta.enabled) {
// FW 1.10+: Reset STA list, force WiFi rescan and connect to stringest AP
list.put(ACTION_RESSTA, "Reconnect WiFi");
}
if (profile.settings.apRoaming != null) {
list.put(!profile.settings.apRoaming.enabled ? ACTION_ENAPROAMING : ACTION_DISAPROAMING,
!profile.settings.apRoaming.enabled ? "Enable WiFi Roaming" : "Disable WiFi Roaming");
}
if (profile.settings.wifiRecoveryReboot != null) {
list.put(!profile.settings.wifiRecoveryReboot ? ACTION_ENWIFIREC : ACTION_DISWIFIREC,
!profile.settings.wifiRecoveryReboot ? "Enable WiFi Recovery" : "Disable WiFi Recovery");
}
boolean set = (profile.settings.cloud != null) && profile.settings.cloud.enabled;
list.put(set ? ACTION_DISCLOUD : ACTION_ENCLOUD, set ? "Disable Cloud" : "Enable Cloud");
list.put(ACTION_RESET, "-Factory Reset");
if (profile.extFeatures) {
list.put(ACTION_OTACHECK, "Check for Update");
boolean debug_enable = getBool(profile.settings.debug_enable);
list.put(!debug_enable ? ACTION_ENDEBUG : ACTION_DISDEBUG,
!debug_enable ? "Enable Debug" : "Disable Debug");
if (debug_enable) {
list.put(ACTION_GETDEB, "Get Debug log");
list.put(ACTION_GETDEB1, "Get Debug log1");
}
}
return list;
}
private String buildActionUrl(String uid, String action) {
return SHELLY_MGR_ACTION_URI + "?" + URLPARM_ACTION + "=" + action + "&" + URLPARM_UID + "=" + urlEncode(uid)
+ "&" + URLPARM_UPDATE + "=yes";
}
private void setRestarted(ShellyManagerInterface th, String uid) {
th.setThingOffline(ThingStatusDetail.GONE, "offline.status-error-restarted");
scheduleUpdate(th, uid + "_upgrade", 25); // wait 25s before refresh
}
}

View File

@@ -0,0 +1,100 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.shelly.internal.manager;
import java.util.Date;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* {@link ShellyManagerCache} implements a cache with expiring times of the entries
*
* @author Markus Michels - Initial contribution
*/
@NonNullByDefault
public class ShellyManagerCache<K, V> extends ConcurrentHashMap<K, V> {
private static final long serialVersionUID = 1L;
private Map<K, Long> timeMap = new ConcurrentHashMap<K, Long>();
private long expiryInMillis = ShellyManagerConstants.CACHE_TIMEOUT_DEF_MIN * 60 * 1000; // Default 1h
public ShellyManagerCache() {
initialize();
}
public ShellyManagerCache(long expiryInMillis) {
this.expiryInMillis = expiryInMillis;
initialize();
}
void initialize() {
new CleanerThread().start();
}
@Override
public @Nullable V put(K key, V value) {
Date date = new Date();
timeMap.put(key, date.getTime());
V returnVal = super.put(key, value);
return returnVal;
}
@Override
public void putAll(@Nullable Map<? extends K, ? extends V> m) {
if (m == null) {
throw new IllegalArgumentException();
}
for (K key : m.keySet()) {
V value = m.get(key);
if (value != null) { // don't allow null values
put(key, value);
}
}
}
@Override
public @Nullable V putIfAbsent(K key, V value) {
if (!containsKey(key)) {
return put(key, value);
} else {
return get(key);
}
}
class CleanerThread extends Thread {
@Override
public void run() {
while (true) {
cleanMap();
try {
Thread.sleep(expiryInMillis / 2);
} catch (InterruptedException e) {
}
}
}
private void cleanMap() {
long currentTime = new Date().getTime();
for (K key : timeMap.keySet()) {
if (currentTime > (timeMap.get(key) + expiryInMillis)) {
V value = remove(key);
timeMap.remove(key);
}
}
}
}
}

View File

@@ -0,0 +1,149 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.shelly.internal.manager;
import java.nio.charset.StandardCharsets;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* {@link ShellyManagerConstants} defines the constants for Shelly Manager
*
* @author Markus Michels - Initial contribution
*/
@NonNullByDefault
public class ShellyManagerConstants {
public static final String LOG_PREFIX = "ShellyManager";
public static final String UTF_8 = StandardCharsets.UTF_8.toString();
public static final String SHELLY_MANAGER_URI = "/shelly/manager";
public static final String SHELLY_MGR_OVERVIEW_URI = SHELLY_MANAGER_URI + "/ovierview";
public static final String SHELLY_MGR_FWUPDATE_URI = SHELLY_MANAGER_URI + "/fwupdate";
public static final String SHELLY_MGR_IMAGES_URI = SHELLY_MANAGER_URI + "/images";
public static final String SHELLY_MGR_ACTION_URI = SHELLY_MANAGER_URI + "/action";
public static final String SHELLY_MGR_OTA_URI = SHELLY_MANAGER_URI + "/ota";
public static final String ACTION_REFRESH = "refresh";
public static final String ACTION_RESTART = "restart";
public static final String ACTION_PROTECT = "protect";
public static final String ACTION_SETCOIOT_PEER = "setcoiotpeer";
public static final String ACTION_SETCOIOT_MCAST = "setcoiotmcast";
public static final String ACTION_SETTZ = "settz";
public static final String ACTION_SETNTP = "setntp";
public static final String ACTION_ENCLOUD = "encloud";
public static final String ACTION_DISCLOUD = "discloud";
public static final String ACTION_RES_STATS = "reset_stat";
public static final String ACTION_RESET = "reset";
public static final String ACTION_RESSTA = "resetsta";
public static final String ACTION_ENWIFIREC = "enwifirec";
public static final String ACTION_DISWIFIREC = "diswifirec";
public static final String ACTION_ENAPROAMING = "enaproaming";
public static final String ACTION_DISAPROAMING = "disaproaming";
public static final String ACTION_OTACHECK = "otacheck";
public static final String ACTION_ENDEBUG = "endebug";
public static final String ACTION_DISDEBUG = "disdebug";
public static final String ACTION_GETDEB = "getdebug";
public static final String ACTION_GETDEB1 = "getdebug1";
public static final String ACTION_NONE = "-";
public static final String TEMPLATE_PATH = "sniplets/";
public static final String HEADER_HTML = "header.html";
public static final String OVERVIEW_HTML = "overview.html";
public static final String OVERVIEW_HEADER = "ov_header.html";
public static final String OVERVIEW_DEVICE = "ov_device.html";
public static final String OVERVIEW_FOOTER = "ov_footer.html";
public static final String FWUPDATE1_HTML = "fw_update1.html";
public static final String FWUPDATE2_HTML = "fw_update2.html";
public static final String ACTION_HTML = "action.html";
public static final String FOOTER_HTML = "footer.html";
public static final String IMAGE_PATH = "images/";
public static final String FORWARD_SCRIPT = "forward.script";
public static final String ATTRIBUTE_METATAG = "metaTag";
public static final String ATTRIBUTE_CSS_HEADER = "cssHeader";
public static final String ATTRIBUTE_CSS_FOOTER = "cssFooter";
public static final String ATTRIBUTE_URI = "uri";
public static final String ATTRIBUTE_UID = "uid";
public static final String ATTRIBUTE_REFRESH = "refreshTimer";
public static final String ATTRIBUTE_MESSAGE = "message";
public static final String ATTRIBUTE_TOTAL_DEV = "totalDevices";
public static final String ATTRIBUTE_STATUS_ICON = "iconStatus";
public static final String ATTRIBUTE_DISPLAY_NAME = "displayName";
public static final String ATTRIBUTE_DEV_STATUS = "deviceStatus";
public static final String ATTRIBUTE_DEBUG_MODE = "debugMode";
public static final String ATTRIBUTE_FIRMWARE_SEL = "firmwareSelection";
public static final String ATTRIBUTE_ACTION_LIST = "actionList";
public static final String ATTRIBUTE_VERSION = "version";
public static final String ATTRIBUTE_FW_URL = "firmwareUrl";
public static final String ATTRIBUTE_UPDATE_URL = "updateUrl";
public static final String ATTRIBUTE_LAST_ALARM = "lastAlarmTs";
public static final String ATTRIBUTE_ACTION = "action";
public static final String ATTRIBUTE_ACTION_BUTTON = "actionButtonLabel";
public static final String ATTRIBUTE_ACTION_URL = "actionUrl";
public static final String ATTRIBUTE_SNTP_SERVER = "sntpServer";
public static final String ATTRIBUTE_COIOT_STATUS = "coiotStatus";
public static final String ATTRIBUTE_COIOT_PEER = "coiotDestination";
public static final String ATTRIBUTE_CLOUD_STATUS = "cloudStatus";
public static final String ATTRIBUTE_MQTT_STATUS = "mqttStatus";
public static final String ATTRIBUTE_ACTIONS_SKIPPED = "actionsSkipped";
public static final String ATTRIBUTE_DISCOVERABLE = "discoverable";
public static final String ATTRIBUTE_WIFI_RECOVERY = "wifiAutoRecovery";
public static final String ATTRIBUTE_APR_MODE = "apRoamingMode";
public static final String ATTRIBUTE_APR_TRESHOLD = "apRoamingThreshold";
public static final String ATTRIBUTE_MAX_ITEMP = "maxInternalTemp";
public static final String ATTRIBUTE_TIMEZONE = "deviceTimezone";
public static final String ATTRIBUTE_PWD_PROTECT = "passwordProtected";
public static final String URLPARM_UID = "uid";
public static final String URLPARM_DEVTYPE = "deviceType";
public static final String URLPARM_DEVMODE = "deviceMode";
public static final String URLPARM_ACTION = "action";
public static final String URLPARM_FILTER = "filter";
public static final String URLPARM_TYPE = "type";
public static final String URLPARM_VERSION = "version";
public static final String URLPARM_UPDATE = "update";
public static final String URLPARM_CONNECTION = "connection";
public static final String URLPARM_URL = "url";
public static final String FILTER_ONLINE = "online";
public static final String FILTER_INACTIVE = "inactive";
public static final String FILTER_ATTENTION = "attention";
public static final String FILTER_UPDATE = "update";
public static final String FILTER_UNPROTECTED = "unprotected";
// Message classes for visual style
public static final String MCMESSAGE = "message";
public static final String MCINFO = "info";
public static final String MCWARNING = "warning";
public static final String ICON_ONLINE = "online";
public static final String ICON_OFFLINE = "offline";
public static final String ICON_UNINITIALIZED = "uninitialized";
public static final String ICON_CONFIG = "config";
public static final String ICON_ATTENTION = "attention";
public static final String CONNECTION_TYPE_LOCAL = "local";
public static final String CONNECTION_TYPE_INTERNET = "internet";
public static final String CONNECTION_TYPE_CUSTOM = "custom";
public static final String FWPROD = "prod";
public static final String FWBETA = "beta";
public static final String FWREPO_PROD_URL = "https://api.shelly.cloud/files/firmware/";
public static final String FWREPO_TEST_URL = "https://repo.shelly.cloud/files/firmware/";
public static final String FWREPO_ARCH_URL = "http://archive.shelly-tools.de/archive.php";
public static final String FWREPO_ARCFILE_URL = "http://archive.shelly-tools.de/version/";
public static final int CACHE_TIMEOUT_DEF_MIN = 60; // Default timeout for cache entries
public static final int CACHE_TIMEOUT_FW_MIN = 15; // Cache entries for the firmware list 15min
}

View File

@@ -0,0 +1,70 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.shelly.internal.manager;
import static org.openhab.binding.shelly.internal.manager.ShellyManagerConstants.IMAGE_PATH;
import static org.openhab.binding.shelly.internal.util.ShellyUtils.substringAfter;
import java.io.IOException;
import java.io.InputStream;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.http.HttpStatus;
import org.openhab.binding.shelly.internal.ShellyHandlerFactory;
import org.openhab.binding.shelly.internal.api.ShellyApiException;
import org.openhab.binding.shelly.internal.handler.ShellyManagerInterface;
import org.openhab.binding.shelly.internal.provider.ShellyTranslationProvider;
import org.osgi.service.cm.ConfigurationAdmin;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* {@link ShellyManagerImageLoader} implements the Shelly Manager's download proxy for images (load them from bundle)
*
* @author Markus Michels - Initial contribution
*/
@NonNullByDefault
public class ShellyManagerImageLoader extends ShellyManagerPage {
private final Logger logger = LoggerFactory.getLogger(ShellyManagerImageLoader.class);
public ShellyManagerImageLoader(ConfigurationAdmin configurationAdmin,
ShellyTranslationProvider translationProvider, HttpClient httpClient, String localIp, int localPort,
ShellyHandlerFactory handlerFactory) {
super(configurationAdmin, translationProvider, httpClient, localIp, localPort, handlerFactory);
}
@Override
public ShellyMgrResponse generateContent(String path, Map<String, String[]> parameters) throws ShellyApiException {
return loadImage(substringAfter(path, ShellyManagerConstants.SHELLY_MGR_IMAGES_URI + "/"));
}
protected ShellyMgrResponse loadImage(String image) throws ShellyApiException {
String file = IMAGE_PATH + image;
logger.trace("Read Image from {}", file);
ClassLoader cl = ShellyManagerInterface.class.getClassLoader();
if (cl != null) {
try (InputStream inputStream = cl.getResourceAsStream(file)) {
if (inputStream != null) {
byte[] buf = new byte[inputStream.available()];
inputStream.read(buf);
return new ShellyMgrResponse(buf, HttpStatus.OK_200, "image/png");
}
} catch (IOException | RuntimeException e) {
logger.debug("ShellyManager: Unable to read {} from bundle resources!", image, e);
}
}
return new ShellyMgrResponse("Unable to read " + image + " from bundle resources!", HttpStatus.NOT_FOUND_404);
}
}

View File

@@ -0,0 +1,221 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.shelly.internal.manager;
import static org.openhab.binding.shelly.internal.ShellyBindingConstants.SHELLY_API_TIMEOUT_MS;
import static org.openhab.binding.shelly.internal.manager.ShellyManagerConstants.*;
import static org.openhab.binding.shelly.internal.util.ShellyUtils.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.TreeMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.HttpStatus;
import org.openhab.binding.shelly.internal.ShellyHandlerFactory;
import org.openhab.binding.shelly.internal.api.ShellyApiException;
import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsUpdate;
import org.openhab.binding.shelly.internal.api.ShellyDeviceProfile;
import org.openhab.binding.shelly.internal.api.ShellyHttpApi;
import org.openhab.binding.shelly.internal.config.ShellyThingConfiguration;
import org.openhab.binding.shelly.internal.handler.ShellyManagerInterface;
import org.openhab.binding.shelly.internal.provider.ShellyTranslationProvider;
import org.openhab.core.thing.ThingStatusDetail;
import org.osgi.service.cm.ConfigurationAdmin;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* {@link ShellyManagerOtaPage} implements the Shelly Manager's download proxy for images (load them from bundle)
*
* @author Markus Michels - Initial contribution
*/
@NonNullByDefault
public class ShellyManagerOtaPage extends ShellyManagerPage {
protected final Logger logger = LoggerFactory.getLogger(ShellyManagerOtaPage.class);
public ShellyManagerOtaPage(ConfigurationAdmin configurationAdmin, ShellyTranslationProvider translationProvider,
HttpClient httpClient, String localIp, int localPort, ShellyHandlerFactory handlerFactory) {
super(configurationAdmin, translationProvider, httpClient, localIp, localPort, handlerFactory);
}
@Override
public ShellyMgrResponse generateContent(String path, Map<String, String[]> parameters) throws ShellyApiException {
if (path.contains(SHELLY_MGR_OTA_URI)) {
return loadFirmware(path, parameters);
} else {
return generatePage(path, parameters);
}
}
public ShellyMgrResponse generatePage(String path, Map<String, String[]> parameters) throws ShellyApiException {
String uid = getUrlParm(parameters, URLPARM_UID);
String version = getUrlParm(parameters, URLPARM_VERSION);
String update = getUrlParm(parameters, URLPARM_UPDATE);
String connection = getUrlParm(parameters, URLPARM_CONNECTION);
String url = getUrlParm(parameters, URLPARM_URL);
if (uid.isEmpty() || (version.isEmpty() && connection.isEmpty()) || !getThingHandlers().containsKey(uid)) {
return new ShellyMgrResponse("Invalid URL parameters: " + parameters, HttpStatus.BAD_REQUEST_400);
}
Map<String, String> properties = new HashMap<>();
String html = loadHTML(HEADER_HTML, properties);
ShellyManagerInterface th = getThingHandlers().get(uid);
if (th != null) {
properties = fillProperties(new HashMap<>(), uid, th);
ShellyThingConfiguration config = getThingConfig(th, properties);
ShellyDeviceProfile profile = th.getProfile();
String deviceType = getDeviceType(properties);
String uri = !url.isEmpty() && connection.equals(CONNECTION_TYPE_CUSTOM) ? url
: getFirmwareUrl(config.deviceIp, deviceType, profile.mode, version,
connection.equals(CONNECTION_TYPE_LOCAL));
if (connection.equalsIgnoreCase(CONNECTION_TYPE_INTERNET)) {
// If target
// - contains "update=xx" then use -> ?update=true for release and ?beta=true for beta
// - otherwise qualify full url with ?url=xxxx
if (uri.contains("update=") || uri.contains("beta=")) {
url = uri;
} else {
url = URLPARM_URL + "=" + uri;
}
} else if (connection.equalsIgnoreCase(CONNECTION_TYPE_LOCAL)) {
// redirect to local server -> http://<oh-ip>:<oh-port>/shelly/manager/ota?deviceType=xxx&version=xxx
String modeParm = !profile.mode.isEmpty() ? "&" + URLPARM_DEVMODE + "=" + profile.mode : "";
url = URLPARM_URL + "=http://" + localIp + ":" + localPort + SHELLY_MGR_OTA_URI + urlEncode(
"?" + URLPARM_DEVTYPE + "=" + deviceType + modeParm + "&" + URLPARM_VERSION + "=" + version);
} else if (connection.equalsIgnoreCase(CONNECTION_TYPE_CUSTOM)) {
// else custom -> don't modify url
uri = url;
url = URLPARM_URL + "=" + uri;
}
String updateUrl = url;
properties.put(ATTRIBUTE_VERSION, version);
properties.put(ATTRIBUTE_FW_URL, uri);
properties.put(ATTRIBUTE_UPDATE_URL, "http://" + getDeviceIp(properties) + "/ota?" + updateUrl);
properties.put(URLPARM_CONNECTION, connection);
if (update.equalsIgnoreCase("yes")) {
// do the update
th.setThingOffline(ThingStatusDetail.FIRMWARE_UPDATING, "offline.status-error-fwupgrade");
html += loadHTML(FWUPDATE2_HTML, properties);
new Thread(() -> { // schedule asynchronous reboot
try {
ShellyHttpApi api = new ShellyHttpApi(uid, config, httpClient);
ShellySettingsUpdate result = api.firmwareUpdate(updateUrl);
String status = getString(result.status);
logger.info("{}: {}", th.getThingName(), getMessage("fwupdate.initiated", status));
// Shelly Motion needs almost 2min for upgrade
scheduleUpdate(th, uid + "_upgrade", profile.isMotion ? 110 : 30);
} catch (ShellyApiException e) {
// maybe the device restarts before returning the http response
logger.warn("{}: {}", th.getThingName(), getMessage("fwupdate.initiated", e.toString()));
}
}).start();
} else {
String message = getMessageP("fwupdate.confirm", MCINFO);
properties.put(ATTRIBUTE_MESSAGE, message);
html += loadHTML(FWUPDATE1_HTML, properties);
}
}
html += loadHTML(FOOTER_HTML, properties);
return new ShellyMgrResponse(html, HttpStatus.OK_200);
}
protected ShellyMgrResponse loadFirmware(String path, Map<String, String[]> parameters) throws ShellyApiException {
String deviceType = getUrlParm(parameters, URLPARM_DEVTYPE);
String deviceMode = getUrlParm(parameters, URLPARM_DEVMODE);
String version = getUrlParm(parameters, URLPARM_VERSION);
String url = getUrlParm(parameters, URLPARM_URL);
logger.info("ShellyManager: {}", getMessage("fwupdate.info", deviceType, version, url));
String failure = getMessage("fwupdate.notfound", deviceType, version, url);
try {
if (url.isEmpty()) {
url = getFirmwareUrl("", deviceType, deviceMode, version, true);
if (url.isEmpty()) {
logger.warn("ShellyManager: {}", failure);
return new ShellyMgrResponse(failure, HttpStatus.BAD_REQUEST_400);
}
}
logger.debug("ShellyManager: Loading firmware from {}", url);
// BufferedInputStream in = new BufferedInputStream(new URL(url).openStream());
// byte[] buf = new byte[in.available()];
// in.read(buf);
Request request = httpClient.newRequest(url).method(HttpMethod.GET).timeout(SHELLY_API_TIMEOUT_MS,
TimeUnit.MILLISECONDS);
ContentResponse contentResponse = request.send();
HttpFields fields = contentResponse.getHeaders();
Map<String, String> headers = new TreeMap<>();
String etag = getString(fields.get("ETag"));
String ranges = getString(fields.get("accept-ranges"));
String modified = getString(fields.get("Last-Modified"));
headers.put("ETag", etag);
headers.put("accept-ranges", ranges);
headers.put("Last-Modified", modified);
byte[] data = contentResponse.getContent();
logger.info("ShellyManager: {}", getMessage("fwupdate.success", data.length, etag, modified));
return new ShellyMgrResponse(data, HttpStatus.OK_200, contentResponse.getMediaType(), headers);
} catch (ExecutionException | TimeoutException | InterruptedException | RuntimeException e) {
logger.info("ShellyManager: {}", failure, e);
return new ShellyMgrResponse(failure, HttpStatus.BAD_REQUEST_400);
}
}
protected String getFirmwareUrl(String deviceIp, String deviceType, String mode, String version, boolean local)
throws ShellyApiException {
switch (version) {
case FWPROD:
case FWBETA:
boolean prod = version.equals(FWPROD);
if (!local) {
// run regular device update
return prod ? "update=true" : "beta=true";
} else {
// convert prod/beta to full url
FwRepoEntry fw = getFirmwareRepoEntry(deviceType, mode);
String url = getString(prod ? fw.url : fw.beta_url);
logger.debug("ShellyManager: Map {} release to url {}, version {}", url,
prod ? fw.url : fw.beta_url, prod ? fw.version : fw.beta_ver);
return url;
}
default: // Update from firmware archive
FwArchList list = getFirmwareArchiveList(deviceType);
ArrayList<FwArchEntry> versions = list.versions;
if (versions != null) {
for (FwArchEntry e : versions) {
String url = FWREPO_ARCFILE_URL + version + "/" + getString(e.file);
if (getString(e.version).equalsIgnoreCase(version)) {
return url;
}
}
}
}
return "";
}
}

View File

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

View File

@@ -0,0 +1,591 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.shelly.internal.manager;
import static org.openhab.binding.shelly.internal.ShellyBindingConstants.*;
import static org.openhab.binding.shelly.internal.manager.ShellyManagerConstants.*;
import static org.openhab.binding.shelly.internal.util.ShellyUtils.*;
import static org.openhab.core.thing.Thing.*;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.Timer;
import java.util.TimerTask;
import java.util.TreeMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.stream.Collectors;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.HttpStatus;
import org.openhab.binding.shelly.internal.ShellyHandlerFactory;
import org.openhab.binding.shelly.internal.api.ShellyApiException;
import org.openhab.binding.shelly.internal.api.ShellyApiResult;
import org.openhab.binding.shelly.internal.api.ShellyDeviceProfile;
import org.openhab.binding.shelly.internal.api.ShellyHttpApi;
import org.openhab.binding.shelly.internal.config.ShellyBindingConfiguration;
import org.openhab.binding.shelly.internal.config.ShellyThingConfiguration;
import org.openhab.binding.shelly.internal.handler.ShellyDeviceStats;
import org.openhab.binding.shelly.internal.handler.ShellyManagerInterface;
import org.openhab.binding.shelly.internal.provider.ShellyTranslationProvider;
import org.openhab.core.library.types.DateTimeType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.types.State;
import org.openhab.core.types.UnDefType;
import org.osgi.service.cm.Configuration;
import org.osgi.service.cm.ConfigurationAdmin;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
/**
* {@link ShellyManagerOtaPage} implements the Shelly Manager's page template
*
* @author Markus Michels - Initial contribution
*/
@NonNullByDefault
public class ShellyManagerPage {
private final Logger logger = LoggerFactory.getLogger(ShellyManagerPage.class);
protected final ShellyTranslationProvider resources;
private final ShellyHandlerFactory handlerFactory;
protected final HttpClient httpClient;
protected final ConfigurationAdmin configurationAdmin;
protected final ShellyBindingConfiguration bindingConfig = new ShellyBindingConfiguration();
protected final String localIp;
protected final int localPort;
protected final Map<String, String> htmlTemplates = new HashMap<>();
protected final Gson gson = new Gson();
protected final ShellyManagerCache<String, FwRepoEntry> firmwareRepo = new ShellyManagerCache<>(15 * 60 * 1000);
protected final ShellyManagerCache<String, FwArchList> firmwareArch = new ShellyManagerCache<>(15 * 60 * 1000);
public static class ShellyMgrResponse {
public @Nullable Object data = "";
public String mimeType = "";
public String redirectUrl = "";
public int code;
public Map<String, String> headers = new HashMap<>();
public ShellyMgrResponse() {
init("", HttpStatus.OK_200, "text/html", null);
}
public ShellyMgrResponse(Object data, int code) {
init(data, code, "text/html", null);
}
public ShellyMgrResponse(Object data, int code, String mimeType) {
init(data, code, mimeType, null);
}
public ShellyMgrResponse(Object data, int code, String mimeType, Map<String, String> headers) {
init(data, code, mimeType, headers);
}
private void init(Object message, int code, String mimeType, @Nullable Map<String, String> headers) {
this.data = message;
this.code = code;
this.mimeType = mimeType;
this.headers = headers != null ? headers : new TreeMap<>();
}
public void setRedirect(String redirectUrl) {
this.redirectUrl = redirectUrl;
}
}
public static class FwArchEntry {
// {"version":"v1.5.10","file":"SHSW-1.zip"}
public @Nullable String version;
public @Nullable String file;
}
public static class FwArchList {
public @Nullable ArrayList<FwArchEntry> versions;
}
public static class FwRepoEntry {
public @Nullable String url; // prod
public @Nullable String version;
public @Nullable String beta_url; // beta version if avilable
public @Nullable String beta_ver;
}
public ShellyManagerPage(ConfigurationAdmin configurationAdmin, ShellyTranslationProvider translationProvider,
HttpClient httpClient, String localIp, int localPort, ShellyHandlerFactory handlerFactory) {
this.configurationAdmin = configurationAdmin;
this.resources = translationProvider;
this.handlerFactory = handlerFactory;
this.httpClient = httpClient;
this.localIp = localIp;
this.localPort = localPort;
}
public ShellyMgrResponse generateContent(String path, Map<String, String[]> parameters) throws ShellyApiException {
return new ShellyMgrResponse("Invalid Request", HttpStatus.BAD_REQUEST_400);
}
protected String loadHTML(String template) throws ShellyApiException {
if (htmlTemplates.containsKey(template)) {
return getString(htmlTemplates.get(template));
}
String html = "";
String file = TEMPLATE_PATH + template;
logger.debug("Read HTML from {}", file);
ClassLoader cl = ShellyManagerInterface.class.getClassLoader();
if (cl != null) {
try (InputStream inputStream = cl.getResourceAsStream(file)) {
if (inputStream != null) {
html = new BufferedReader(new InputStreamReader(inputStream)).lines()
.collect(Collectors.joining("\n"));
htmlTemplates.put(template, html);
}
} catch (IOException e) {
throw new ShellyApiException("Unable to read " + file + " from bundle resources!", e);
}
}
return html;
}
protected String loadHTML(String template, Map<String, String> properties) throws ShellyApiException {
properties.put(ATTRIBUTE_URI, SHELLY_MANAGER_URI);
String html = loadHTML(template);
return fillAttributes(html, properties);
}
protected Map<String, String> fillProperties(Map<String, String> properties, String uid,
ShellyManagerInterface th) {
try {
Configuration serviceConfig = configurationAdmin.getConfiguration("binding." + BINDING_ID);
bindingConfig.updateFromProperties(serviceConfig.getProperties());
} catch (IOException e) {
logger.debug("ShellyManager: Unable to get bindingConfig");
}
properties.putAll(th.getThing().getProperties());
Thing thing = th.getThing();
ThingStatus status = thing.getStatus();
properties.put("thingName", getString(thing.getLabel()));
properties.put("thingStatus", status.toString());
ThingStatusDetail detail = thing.getStatusInfo().getStatusDetail();
properties.put("thingStatusDetail", detail.equals(ThingStatusDetail.NONE) ? "" : getString(detail.toString()));
properties.put("thingStatusDescr", getString(thing.getStatusInfo().getDescription()));
properties.put(ATTRIBUTE_UID, uid);
ShellyDeviceProfile profile = th.getProfile();
ShellyThingConfiguration config = thing.getConfiguration().as(ShellyThingConfiguration.class);
ShellyDeviceStats stats = th.getStats();
properties.putAll(stats.asProperties());
for (Map.Entry<String, Object> p : thing.getConfiguration().getProperties().entrySet()) {
String key = p.getKey();
if (p.getValue() != null) {
String value = p.getValue().toString();
properties.put(key, value);
}
}
State state = th.getChannelValue(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_NAME);
if (state != UnDefType.NULL) {
addAttribute(properties, th, CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_NAME);
} else {
// If the Shelly doesn't provide a device name (not configured) we use the service name
String deviceName = getDeviceName(properties);
properties.put(PROPERTY_DEV_NAME,
!deviceName.isEmpty() ? deviceName : getString(properties.get(PROPERTY_SERVICE_NAME)));
}
if (config.userId.isEmpty()) {
// Get defauls from Binding Config
properties.put("userId", bindingConfig.defaultUserId);
properties.put("password", bindingConfig.defaultPassword);
}
addAttribute(properties, th, CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_RSSI);
addAttribute(properties, th, CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_UPTIME);
addAttribute(properties, th, CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_HEARTBEAT);
addAttribute(properties, th, CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_ITEMP);
addAttribute(properties, th, CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_WAKEUP);
addAttribute(properties, th, CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_CHARGER);
addAttribute(properties, th, CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_UPDATE);
addAttribute(properties, th, CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_ALARM);
addAttribute(properties, th, CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_CHARGER);
properties.put(ATTRIBUTE_DEBUG_MODE, getOption(profile.settings.debug_enable));
properties.put(ATTRIBUTE_DISCOVERABLE, String.valueOf(getBool(profile.settings.discoverable)));
properties.put(ATTRIBUTE_WIFI_RECOVERY, String.valueOf(getBool(profile.settings.wifiRecoveryReboot)));
properties.put(ATTRIBUTE_APR_MODE,
profile.settings.apRoaming != null ? getOption(profile.settings.apRoaming.enabled) : "n/a");
properties.put(ATTRIBUTE_APR_TRESHOLD,
profile.settings.apRoaming != null ? getOption(profile.settings.apRoaming.threshold) : "n/a");
properties.put(ATTRIBUTE_PWD_PROTECT,
profile.auth ? "enabled, user=" + getString(profile.settings.login.username) : "disabled");
String tz = getString(profile.settings.timezone);
properties.put(ATTRIBUTE_TIMEZONE,
(tz.isEmpty() ? "n/a" : tz) + ", auto-detect: " + getBool(profile.settings.tzautodetect));
properties.put(ATTRIBUTE_ACTIONS_SKIPPED,
profile.status.astats != null ? String.valueOf(profile.status.astats.skipped) : "n/a");
properties.put(ATTRIBUTE_MAX_ITEMP, stats.maxInternalTemp > 0 ? stats.maxInternalTemp + " °C" : "n/a");
// Shelly H&T: When external power is connected the battery level is not valid
if (!profile.isHT || (getInteger(profile.settings.externalPower) == 0)) {
addAttribute(properties, th, CHANNEL_GROUP_BATTERY, CHANNEL_SENSOR_BAT_LEVEL);
} else {
properties.put(CHANNEL_SENSOR_BAT_LEVEL, "USB");
}
String wiFiSignal = getString(properties.get(CHANNEL_DEVST_RSSI));
if (!wiFiSignal.isEmpty()) {
properties.put("wifiSignalRssi", wiFiSignal + " / " + stats.wifiRssi + " dBm");
properties.put("imgWiFi", "imgWiFi" + wiFiSignal);
}
if (profile.settings.sntp != null) {
properties.put(ATTRIBUTE_SNTP_SERVER,
getString(profile.settings.sntp.server) + ", enabled: " + getBool((profile.settings.sntp.enabled)));
}
boolean coiotEnabled = true;
if ((profile.settings.coiot != null) && (profile.settings.coiot.enabled != null)) {
coiotEnabled = profile.settings.coiot.enabled;
}
properties.put(ATTRIBUTE_COIOT_STATUS,
!coiotEnabled ? "Disbaled in settings" : "Events are " + (config.eventsCoIoT ? "enabled" : "disabled"));
properties.put(ATTRIBUTE_COIOT_PEER,
(profile.settings.coiot != null) && !getString(profile.settings.coiot.peer).isEmpty()
? profile.settings.coiot.peer
: "Multicast");
if (profile.status.cloud != null) {
properties.put(ATTRIBUTE_CLOUD_STATUS,
getBool(profile.settings.cloud.enabled)
? getBool(profile.status.cloud.connected) ? "connected" : "enabled"
: "disabled");
} else {
properties.put(ATTRIBUTE_CLOUD_STATUS, "unknown");
}
if (profile.status.mqtt != null) {
properties.put(ATTRIBUTE_MQTT_STATUS,
getBool(profile.settings.mqtt.enable)
? getBool(profile.status.mqtt.connected) ? "connected" : "enabled"
: "disabled");
} else {
properties.put(ATTRIBUTE_MQTT_STATUS, "unknown");
}
String statusIcon = "";
ThingStatus ts = th.getThing().getStatus();
switch (ts) {
case UNINITIALIZED:
case REMOVED:
case REMOVING:
statusIcon = ICON_UNINITIALIZED;
break;
case OFFLINE:
ThingStatusDetail sd = th.getThing().getStatusInfo().getStatusDetail();
if (uid.contains(THING_TYPE_SHELLYUNKNOWN_STR) || (sd == ThingStatusDetail.CONFIGURATION_ERROR)
|| (sd == ThingStatusDetail.HANDLER_CONFIGURATION_PENDING)) {
statusIcon = ICON_CONFIG;
break;
}
default:
statusIcon = ts.toString();
}
properties.put(ATTRIBUTE_STATUS_ICON, statusIcon.toLowerCase());
return properties;
}
private void addAttribute(Map<String, String> properties, ShellyManagerInterface thingHandler, String group,
String attribute) {
State state = thingHandler.getChannelValue(group, attribute);
String value = "";
if (state != UnDefType.NULL) {
if (state instanceof DateTimeType) {
DateTimeType dt = (DateTimeType) state;
switch (attribute) {
case ATTRIBUTE_LAST_ALARM:
value = dt.format(null).replace('T', ' ').replace('-', '/');
break;
default:
value = getTimestamp(dt);
value = dt.format(null).replace('T', ' ').replace('-', '/');
}
} else {
value = state.toString();
}
}
properties.put(attribute, value);
}
protected String fillAttributes(String template, Map<String, String> properties) {
if (!template.contains("${")) {
// no replacement necessary
return template;
}
String result = template;
for (Map.Entry<String, String> var : properties.entrySet()) {
result = result.replaceAll(java.util.regex.Pattern.quote("${" + var.getKey() + "}"),
getValue(properties, var.getKey()));
}
if (result.contains("${")) {
return result.replaceAll("\\Q${\\E.*}", "");
} else {
return result;
}
}
protected String getValue(Map<String, String> properties, String attribute) {
String value = getString(properties.get(attribute));
if (!value.isEmpty()) {
switch (attribute) {
case PROPERTY_FIRMWARE_VERSION:
value = substringBeforeLast(value, "-");
break;
case PROPERTY_UPDATE_AVAILABLE:
value = value.replace(OnOffType.ON.toString(), "yes");
value = value.replace(OnOffType.OFF.toString(), "no");
break;
case CHANNEL_DEVST_HEARTBEAT:
break;
}
}
return value;
}
protected FwRepoEntry getFirmwareRepoEntry(String deviceType, String mode) throws ShellyApiException {
logger.debug("ShellyManager: Load firmware list from {}", FWREPO_PROD_URL);
FwRepoEntry fw = null;
if (firmwareRepo.containsKey(deviceType)) {
fw = firmwareRepo.get(deviceType);
}
String json = httpGet(FWREPO_PROD_URL); // returns a strange JSON format so we are parsing this manually
String entry = substringBetween(json, "\"" + deviceType + "\":{", "}");
if (!entry.isEmpty()) {
entry = "{" + entry + "}";
/*
* Example:
* "SHPLG-1":{
* "url":"http:\/\/repo.shelly.cloud\/firmware\/SHPLG-1.zip",
* "version":"20201228-092318\/v1.9.3@ad2bb4e3",
* "beta_url":"http:\/\/repo.shelly.cloud\/firmware\/rc\/SHPLG-1.zip",
* "beta_ver":"20201223-093703\/v1.9.3-rc5@3f583801"
* },
*/
fw = fromJson(gson, entry, FwRepoEntry.class);
// Special case: RGW2 has a split firmware - xxx-white.zip vs. xxx-color.zip
if (!mode.isEmpty() && deviceType.equalsIgnoreCase(SHELLYDT_RGBW2)) {
// check for spilt firmware
String url = substringBefore(fw.url, ".zip") + "-" + mode + ".zip";
if (testUrl(url)) {
fw.url = url;
logger.debug("ShellyManager: Release Split-URL for device type {} is {}", deviceType, url);
}
url = substringBefore(fw.beta_url, ".zip") + "-" + mode + ".zip";
if (testUrl(url)) {
fw.beta_url = url;
logger.debug("ShellyManager: Beta Split-URL for device type {} is {}", deviceType, url);
}
}
firmwareRepo.put(deviceType, fw);
}
return fw != null ? fw : new FwRepoEntry();
}
protected FwArchList getFirmwareArchiveList(String deviceType) throws ShellyApiException {
FwArchList list;
String json = "";
if (firmwareArch.contains(deviceType)) {
list = firmwareArch.get(deviceType); // return from cache
if (list != null) {
return list;
}
}
try {
if (!deviceType.isEmpty()) {
json = httpGet(FWREPO_ARCH_URL + "?type=" + deviceType);
}
} catch (ShellyApiException e) {
logger.debug("{}: Unable to get firmware list for device type {}: {}", LOG_PREFIX, deviceType,
e.toString());
}
if (json.isEmpty() || json.startsWith("[]")) {
// no files available for this device type
logger.info("{}: No firmware files found for device type {}", LOG_PREFIX, deviceType);
list = new FwArchList();
list.versions = new ArrayList<FwArchEntry>();
} else {
// Create selection list
json = "{" + json.replace("[{", "\"versions\":[{") + "}"; // make it an named array
list = fromJson(gson, json, FwArchList.class);
}
// save list to cache
firmwareArch.put(deviceType, list);
return list;
}
protected boolean testUrl(String url) {
try {
if (url.isEmpty()) {
return false;
}
httpHeadl(url); // causes exception on 404
return true;
} catch (ShellyApiException e) {
}
return false;
}
protected String httpGet(String url) throws ShellyApiException {
return httpRequest(HttpMethod.GET, url);
}
protected String httpHeadl(String url) throws ShellyApiException {
return httpRequest(HttpMethod.HEAD, url);
}
protected String httpRequest(HttpMethod method, String url) throws ShellyApiException {
ShellyApiResult apiResult = new ShellyApiResult();
try {
Request request = httpClient.newRequest(url).method(method).timeout(SHELLY_API_TIMEOUT_MS,
TimeUnit.MILLISECONDS);
request.header(HttpHeader.ACCEPT, ShellyHttpApi.CONTENT_TYPE_JSON);
logger.trace("{}: HTTP {} {}", LOG_PREFIX, method, url);
ContentResponse contentResponse = request.send();
apiResult = new ShellyApiResult(contentResponse);
String response = contentResponse.getContentAsString().replace("\t", "").replace("\r\n", "").trim();
logger.trace("{}: HTTP Response {}: {}", LOG_PREFIX, contentResponse.getStatus(), response);
// validate response, API errors are reported as Json
if (contentResponse.getStatus() != HttpStatus.OK_200) {
throw new ShellyApiException(apiResult);
}
return response;
} catch (ExecutionException | TimeoutException | InterruptedException | IllegalArgumentException e) {
throw new ShellyApiException("HTTP GET failed", e);
}
}
protected String getUrlParm(Map<String, String[]> parameters, String param) {
String[] p = parameters.get(param);
String value = "";
if (p != null) {
value = getString(p[0]);
}
return value;
}
protected String getMessage(String key, Object... arguments) {
return resources.get("manager." + key, arguments);
}
protected String getMessageP(String key, String msgClass, Object... arguments) {
return "<p class=\"" + msgClass + "\">" + getMessage(key, arguments) + "</p>\n";
}
protected String getMessageS(String key, String msgClass, Object... arguments) {
return "<span class=\"" + msgClass + "\">" + getMessage(key, arguments) + "</span>\n";
}
protected static String getDeviceType(Map<String, String> properties) {
return getString(properties.get(PROPERTY_MODEL_ID));
}
protected static String getDeviceIp(Map<String, String> properties) {
return getString(properties.get("deviceIp"));
}
protected static String getDeviceName(Map<String, String> properties) {
return getString(properties.get(PROPERTY_DEV_NAME));
}
protected static String getOption(@Nullable Boolean option) {
if (option == null) {
return "n/a";
}
return option ? "enabled" : "disabled";
}
protected static String getOption(@Nullable Integer option) {
if (option == null) {
return "n/a";
}
return option.toString();
}
protected static String getDisplayName(Map<String, String> properties) {
String name = getString(properties.get(PROPERTY_DEV_NAME));
if (name.isEmpty()) {
name = getString(properties.get(PROPERTY_SERVICE_NAME));
}
return name;
}
protected ShellyThingConfiguration getThingConfig(ShellyManagerInterface th, Map<String, String> properties) {
Thing thing = th.getThing();
ShellyThingConfiguration config = thing.getConfiguration().as(ShellyThingConfiguration.class);
if (config.userId.isEmpty()) {
config.userId = getString(properties.get("userId"));
config.password = getString(properties.get("password"));
}
return config;
}
protected void scheduleUpdate(ShellyManagerInterface th, String name, int delay) {
TimerTask task = new TimerTask() {
@Override
public void run() {
th.requestUpdates(1, true);
}
};
Timer timer = new Timer(name);
timer.schedule(task, delay * 1000);
}
protected Map<String, ShellyManagerInterface> getThingHandlers() {
return handlerFactory.getThingHandlers();
}
protected @Nullable ShellyManagerInterface getThingHandler(String uid) {
return getThingHandlers().get(uid);
}
}

View File

@@ -0,0 +1,150 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.shelly.internal.manager;
import static org.openhab.binding.shelly.internal.manager.ShellyManagerConstants.*;
import static org.openhab.binding.shelly.internal.util.ShellyUtils.*;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.util.Map;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.shelly.internal.ShellyHandlerFactory;
import org.openhab.binding.shelly.internal.api.ShellyApiException;
import org.openhab.binding.shelly.internal.manager.ShellyManagerPage.ShellyMgrResponse;
import org.openhab.binding.shelly.internal.provider.ShellyTranslationProvider;
import org.openhab.core.io.net.http.HttpClientFactory;
import org.openhab.core.net.HttpServiceUtil;
import org.openhab.core.net.NetworkAddressService;
import org.osgi.service.cm.ConfigurationAdmin;
import org.osgi.service.component.ComponentContext;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.ConfigurationPolicy;
import org.osgi.service.component.annotations.Deactivate;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.http.HttpService;
import org.osgi.service.http.NamespaceException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* {@link ShellyManagerServlet} implements the Shelly Manager - a simple device overview/management
*
* @author Markus Michels - Initial contribution
*/
@NonNullByDefault
@Component(service = HttpServlet.class, configurationPolicy = ConfigurationPolicy.OPTIONAL)
public class ShellyManagerServlet extends HttpServlet {
private static final long serialVersionUID = 1393403713585449126L;
private final Logger logger = LoggerFactory.getLogger(ShellyManagerServlet.class);
private static final String SERVLET_URI = SHELLY_MANAGER_URI;
private final ShellyManager manager;
private final String className;
private final HttpService httpService;
@Activate
public ShellyManagerServlet(@Reference ConfigurationAdmin configurationAdmin,
@Reference NetworkAddressService networkAddressService, @Reference HttpService httpService,
@Reference HttpClientFactory httpClientFactory, @Reference ShellyHandlerFactory handlerFactory,
@Reference ShellyTranslationProvider translationProvider, ComponentContext componentContext,
Map<String, Object> config) {
className = substringAfterLast(getClass().toString(), ".");
this.httpService = httpService;
String localIp = getString(networkAddressService.getPrimaryIpv4HostAddress());
int localPort = HttpServiceUtil.getHttpServicePort(componentContext.getBundleContext());
this.manager = new ShellyManager(configurationAdmin, translationProvider,
httpClientFactory.getCommonHttpClient(), localIp, localPort, handlerFactory);
try {
httpService.registerServlet(SERVLET_URI, this, null, httpService.createDefaultHttpContext());
logger.debug("{}: Started at '{}'", className, SERVLET_URI);
} catch (NamespaceException | ServletException | IllegalArgumentException e) {
logger.warn("{}: Unable to initialize bindingConfig", className, e);
}
}
@Deactivate
protected void deactivate() {
httpService.unregister(SERVLET_URI);
logger.debug("{} stopped", className);
}
@Override
protected void service(@Nullable HttpServletRequest request, @Nullable HttpServletResponse response)
throws ServletException, IOException, IllegalArgumentException {
if ((request == null) || (response == null)) {
logger.debug("request or resp must not be null!");
return;
}
String path = getString(request.getRequestURI()).toLowerCase();
String ipAddress = request.getHeader("HTTP_X_FORWARDED_FOR");
ShellyMgrResponse output = new ShellyMgrResponse();
PrintWriter print = null;
OutputStream bin = null;
try {
if (ipAddress == null) {
ipAddress = request.getRemoteAddr();
}
Map<String, String[]> parameters = request.getParameterMap();
logger.debug("{}: {} Request from {}:{}{}?{}", className, request.getProtocol(), ipAddress,
request.getRemotePort(), path, parameters.toString());
if (!path.toLowerCase().startsWith(SERVLET_URI)) {
logger.warn("{} received unknown request: path = {}", className, path);
return;
}
output = manager.generateContent(path, parameters);
response.setContentType(output.mimeType);
if (output.mimeType.equals("text/html")) {
// Make sure it's UTF-8 encoded
response.setCharacterEncoding(UTF_8);
print = response.getWriter();
print.write((String) output.data);
} else {
// binary data
byte[] data = (byte[]) output.data;
response.setContentLength(data.length);
bin = response.getOutputStream();
bin.write(data, 0, data.length);
}
} catch (ShellyApiException | RuntimeException e) {
logger.debug("{}: Exception uri={}, parameters={}", className, path, request.getParameterMap().toString(),
e);
response.setContentType("text/html");
print = response.getWriter();
print.write("Exception:" + e.toString() + "<br/>Check openHAB.log for details."
+ "<p/><a href=\"/shelly/manager\">Return to Overview</a>");
logger.debug("{}: {}", className, output);
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
} finally {
if (print != null) {
print.close();
}
if (bin != null) {
bin.close();
}
}
}
}

View File

@@ -21,8 +21,8 @@ offline.status-error-restarted = The device has restarted and will be re-initial
offline.status-error-fwupgrade = Firmware upgrade in progress
message.versioncheck.failed = Unable to check firmware version: {0}
message.versioncheck.beta = Device is running a Beta version: {0}/{1} ({2}),make sure this is newer than {3} release build.
message.versioncheck.tooold = WARNING: Firmware might be too old, installed: {0}/{1} ({2}), required minimal {3}.
message.versioncheck.beta = Device is running a Beta version: {0}/{1}.
message.versioncheck.tooold = WARNING: Firmware might be too old, installed: {0}/{1}, required minimal {3}.
message.versioncheck.update = INFO: New firmware available: current version: {0}, new version: {1}
message.versioncheck.autocoiot = INFO: Firmware is full-filling the minimum version to auto-enable CoIoT
message.roller.calibrating = Device is not calibrated, use Shelly App to perform initial roller calibration.
@@ -71,3 +71,64 @@ channel-type.shelly.ledPowerDisable.label = Disable Power LED
channel-type.shelly.ledPowerDisable.description = ON: The power status LED will be deactivated
channel-type.shelly.ledStatusDisable.label = Disable Status LED
channel-type.shelly.ledStatusDisable.description = ON: The WiFi status LED will be deactivated
# Shelly Manager
message.manager.invalid-url = Invalid URL or syntax
message.manager.buttons.ok = OK
message.manager.buttons.abort = Abort
message.manager.action.unknown = Action {0} is unknown
message.manager.action.reset-stats = Reset Statistics
message.manager.action.restart = Reboot Device
message.manager.action.restart.info = The device is restarting and reconnects to WiFi. It will take a moment until device status is refreshed in openHAB.
message.manager.action.restart.confirm = The device will restart and reconnects to WiFi.
message.manager.action.resstats.confirm = Device statistics and alarm has been reset.
message.manager.action.setcloud.config = Cloud function is now {0}.
message.manager.action.protect = Protect Device
message.manager.action.protect.id-missing = Credentials for device access are not configured, go to Shelly Binding Settings and provide user id and password.<br/>You could use the 'Protect' action to apply this configuration to the device.
message.manager.action.protect.status = Device protection is currently {0}. User id {1} is required to access the device.
message.manager.action.protect.new = Device login will be set to user {0} with password {1}.
message.manager.action.protect.confirm = Device login was updated to user {0} with password {1}.
message.manager.action.could-enable = Enable Cloud
message.manager.action.could-disable = Disable Cloud
message.manager.action.coiot-mcast = Set CoIoT Multicast
message.manager.action.coiot-peer = Set CoIoT Peer
message.manager.action.timezone = Set Timezone
message.manager.action.reset = Factory Reset
message.manager.action.reset.warning = Attention: Performing this action will reset the device to factory defaults.<br/>All configuration data incl. WiFi settings get lost and device will return to Access Point mode (WiFi {0})
message.manager.action.reset.confirm = Factory reset was performed. Connect to WiFi network {0} and open http://192.168.33.1 to restart with device setup.
message.manager.action.checkupd.new = Firmware update available: {0}
message.manager.action.checkupd.ok = Firmware check completed, check device overview for new version.
message.manager.action.checkupd.runnuing = Firmware check was initiated.
message.manager.action.checkupd.failed = Unable to check for firmware update: {0}
message.manager.action.setwifirec-enable = The device performs an auto-restart if WiFi Recovery Mode is enabled and device is facing WiFi connectivity issues.
message.manager.action.setwifirec-disable = WiFi Recovery Mode will be disabled.
message.manager.action.setwifirec-confirm = WiFi Recovery Mode has been {0}.
message.manager.action.setwifirec-failed = Unable to update setting for WiFi Recovery Mode: {0}
message.manager.action.aproaming-enable = WiFi Access Point Roaming will be enabled. Check product documentation for details.
message.manager.action.aproaming-disable = WiFi Access Point Roaming will be disabled.
message.manager.action.aproaming-confirm = Unable to update setting WiFi Access Point Roaming: {0}
message.manager.action.aproaming-failed = Unable to update setting for WiFi Recovery Mode: {0}
message.manager.action.resetsta-info = The WiFi STA/AP Cache will be cleared and the device reconnects to the strongest Access Point.
message.manager.action.resetsta-confirm = Device is reconnecting to the strongest WiFi Access Point.
message.manager.action.resetsta-failed = Unable to clear STA/AP list and reconnect to WiFi: {0}
message.manager.action.debug-enable = Device Debug will be enabled. Use this feature only if requested by Allterco Support.
message.manager.action.debug-disable = Device Debug will be disabled.
message.manager.action.debug-confirm = Device Debug was {0}.
message.manager.action.getdebug-failed = Unable to get Debug Log: {0}
message.manager.coiot.multicast-not-supported = Device doesn't support CoIoT Multicast updates.<br/>Make sure to setup openHAB as CoIoT Peer Address ({0}).
message.manager.coiot.mode-not-suppored = Device doesn't support request CoIoT Mode ({0}), check product documentation.
message.manager.coiot.current-peer = CoIoT Peer Address is currently set to {0}.
message.manager.coiot.new-peer = CoIoT mode/address will be set to {0}.
message.manager.coiot.mode-mcast = The device starts sending CoIoT updates using IP Multicast.<br/>Please make sure that your network setup supports Multicast routing when devices are on different IP subnets.
message.manager.coiot.mode-peer = The device will no longer send IP Multicast CoIoT updates to the network, just to the openHAB host.
message.manager.fwupdate.initiated = Firmware update initiated, device returned status {0}
message.manager.fwupdate.confirm = Do not power-off or restart device while updating the firmware!
message.manager.fwupdate.info = Update firmware (deviceType={0}, version={1}, URL={2})
message.manager.fwupdate.failed = Firmware updated failed: {0}
message.manager.fwupdate.notfound = Unable to find firmware for device type {0}, version={1} (URL={2})
message.manager.fwupdate.nofile = No firmware files found for device type {0}
message.manager.fwupdate.success = Firmware successfully loaded - size={0}, ETag={1}, last modified={2}

View File

@@ -28,8 +28,8 @@ config-status.error.missing-userid = Keine Benutzerkennung in der Thing Konfigur
# General messages
message.versioncheck.failed = Firmware-Version konnte nicht geprüft werden: {0}
message.versioncheck.beta = Es wurde eine Betaversion erkannt: {0}/{1} ({2}), bitte sicherstellen, dass diese neuer ist als Version {3} (Release Build).
message.versioncheck.tooold = ACHTUNG: Eine alte Firmware wurde erkannt: {0}/{1} ({2}), minimal erforderlich {3}.
message.versioncheck.beta = Es wurde eine Betaversion erkannt: {0}/{1}.
message.versioncheck.tooold = ACHTUNG: Eine alte Firmware wurde erkannt: {0}/{1}, minimal erforderlich {2}.
message.versioncheck.update = INFO: Eine neue Firmwareversion ist verfügbar, aktuell: {0}, neu: {1}
message.versioncheck.autocoiot = INFO: Die Firmware unterstützt die Anforderung, Auto-CoIoT wurde aktiviert.
message.init.noipaddress = Es konnte keine lokale IP-Adresse ermittelt werden. Bitte sicherstellen, dass IPv4 aktiviert ist und das richtige Interface in der openHAB Netzwerk-Konfiguration ausgewählt ist.

Binary file not shown.

After

Width:  |  Height:  |  Size: 282 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 603 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 554 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 566 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

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

View File

@@ -0,0 +1,13 @@
</tbody></table>
<p/>
<hr/>
<div class="navFooter">
<a class="navFooter" href="${uri}">Device Overview</a> | <a href="/">openHAB Home</a>
${numberDevices}
</div>
${cssFooter}
<p/>
</body>
</html>

View File

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

View File

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

View File

@@ -0,0 +1,17 @@
<div class="page">
<p class="caption">Firmware Update</p>
<hr/><p/>
<p class="message">Updating device ${deviceName} (${uid}) with version ${version}, connection type=${connection}</p>
<p class="message" sytle>Update url: ${updateUrl}</p>
<span class="top-distance20">
<p class="info">Wait 1-2 minutes, then check device UI at <a href="http://${deviceIp}" title="${thingName}" target="_blank">${deviceIp}</a>, section Firmware.</p>
<p class="warning">Do not power-off or restart device while updating the firmware!</p>
</span>
<p>
<button class="button" onclick="location = '${uri}/overview'">Ok</button>
</p>
</div>
<p/>

View File

@@ -0,0 +1,43 @@
<!DOCTYPE html>
<html lang="en" ng-app="Shelly Manager">
<title>Shelly Manager</title>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
${metaTag}
<style type=text/css>
body {
height:100%, width:100%; padding: 0; margin: 0;
font-family: 'Roboto', Sans-Serif; color: white;
background-color: #262D2F;
}
.page { padding: 6px 6px; }
.navFooter { font-size:12px; padding-top: 2px; }
.footerDevices { float: right; padding-right: 3px; color:#00cc00; }
.top-distance20 { padding-top: 20px; paddin-left: 6px; }
.caption { color:#2886c7; font-size:18px; font-weight: bold; font-style: italic; }
.caption2 { color:#2886c7; font-size:14px; font-weight: bold; font-style: italic; }
.message { padding-left: 6px; }
.blue, .info { color:#2886c7; padding-left: 6px; }
.red, .warning { color:#cb4a4a; padding-left: 6px; }
.green { color:#00cc00; }
a:link { color: #3399ff; background-color: transparent; text-decoration: none; }
a:visited { color: #00cc00; background-color: transparent; text-decoration: none; }
a:hover { color: #00f700; background-color: transparent; text-decoration: underline; }
a:active { color: yellow; background-color: transparent; text-decoration: underline; }
.button { background-color: #FF6600; color: white; border: 0; hight: 14px; min-width: 50px; font-size:14px; padding: 6px 6px; }
.buttonCancel { color: #2886c7; border: 1px; border-color: white; hight: 14px; min-width: 50px; font-size:14px; padding: 6px 6px; }
input[type='radio'], label{ vertical-align: baseline; padding: 4px; margin: 6px; }
input[type='text'] { vertical-align: baseline; color: white; font-size: 12px; background-color: #262D2F; height: 16px; border: 1px solid; border-color: #2886c7;}
.select-hr { border: 1px; border-style: solid; padding-top: 2px; color: white; border-color: white; text-color: white; }
</style>
${cssHeader}
</head>
<body>

View File

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

View File

@@ -0,0 +1,33 @@
<script>
var tooltips = document.querySelectorAll(".tooltip");
tooltips.forEach(function(tooltip, index)
{
tooltip.addEventListener("mouseover", position_tooltip); // On hover, launch the function below
})
function position_tooltip(){
// Get .tooltiptext sibling
var tooltip = this.parentNode.querySelector(".tooltiptext");
// Get calculated tooltip coordinates and size
var tooltip_rect = this.getBoundingClientRect();
var tipX = tooltip_rect.width + 5; // 5px on the right of the tooltip
var tipY = -40; // 40px on the top of the tooltip
// Position tooltip
tooltip.style.top = tipY + 'px';
tooltip.style.left = tipX + 'px';
// Get calculated tooltip coordinates and size
var tooltip_rect = tooltip.getBoundingClientRect();
// Corrections if out of window
if ((tooltip_rect.x + tooltip_rect.width) > window.innerWidth) // Out on the right
tipX = -tooltip_rect.width - 5; // Simulate a "right: tipX" position
if (tooltip_rect.y < 0) // Out on the top
tipY = tipY - tooltip_rect.y; // Align on the top
// Apply corrected position
tooltip.style.top = tipY + 'px';
tooltip.style.left = tipX + 'px';
}
</script>

View File

@@ -0,0 +1,37 @@
<style>
.navigation table { width:100%; vertical-align: middle; text-align:right; float:right; font-size:12px; border: 0; }
.navigation tr { border:0; }
.navigation img { vertical-align: middle; width: 14px; height: 14px; }
.navigation select, option { background-color: #555; color:white; box-sizing: border-box; }
.navigation a:hover { color: transparent; background-color: transparent; text-decoration: none;}
.devTable { border-collapse:collapse;font-size:12px; color:white; }
.devTable th { background-color:#0B398C; }
.devTable tr:nth-child(even) { }
.devTable tr:nth-child(odd) { background: #555; border: 0; }
.devTable td, .devTable th { padding:3px 3px; white-space: wrap; border: 0; }
.devTable select, option { background-color: #555; color:white; box-sizing: border-box; width: 120px; }
.navRefIcon { margin: 0 auto; vertical-align: middle; width: 20px; height: 20px; border: 0;}
.statusIcon { margin: 0 auto; vertical-align: middle; width: 12px; height: 12px; border: 0;}
.icon { margin: 0 auto; vertical-align: middle; width: 14px; height: 16px; border: 0;}
.tooltip { position: relative; display: inline-block; }
.tooltiptext table { padding: 7px 7px; background: #555; border: 0; white-space: nowrap;}
.tooltip:hover .tooltiptext { visibility: visible; opacity: 1; }
.tooltip .tooltiptext {
visibility: hidden;
color: #fff;
text-align: left;
border-radius: 5px;
padding: 5px 5px;
position: absolute;
z-index: 1;
bottom: 125%;
left: 50%;
margin-left: -50%;
opacity: 0;
transition: opacity 0.3s;
}
</style>

View File

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