2 * Copyright (c) 2010-2021 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.shelly.internal.manager;
15 import static org.openhab.binding.shelly.internal.ShellyBindingConstants.*;
16 import static org.openhab.binding.shelly.internal.api.ShellyDeviceProfile.extractFwVersion;
17 import static org.openhab.binding.shelly.internal.manager.ShellyManagerConstants.*;
18 import static org.openhab.binding.shelly.internal.util.ShellyUtils.*;
20 import java.util.ArrayList;
21 import java.util.HashMap;
23 import java.util.TreeMap;
25 import org.eclipse.jdt.annotation.NonNullByDefault;
26 import org.eclipse.jetty.client.HttpClient;
27 import org.eclipse.jetty.http.HttpStatus;
28 import org.openhab.binding.shelly.internal.ShellyHandlerFactory;
29 import org.openhab.binding.shelly.internal.api.ShellyApiException;
30 import org.openhab.binding.shelly.internal.api.ShellyDeviceProfile;
31 import org.openhab.binding.shelly.internal.config.ShellyThingConfiguration;
32 import org.openhab.binding.shelly.internal.handler.ShellyDeviceStats;
33 import org.openhab.binding.shelly.internal.handler.ShellyManagerInterface;
34 import org.openhab.binding.shelly.internal.provider.ShellyTranslationProvider;
35 import org.openhab.binding.shelly.internal.util.ShellyVersionDTO;
36 import org.openhab.core.library.types.DecimalType;
37 import org.openhab.core.library.types.OnOffType;
38 import org.openhab.core.thing.Thing;
39 import org.openhab.core.thing.ThingStatus;
40 import org.openhab.core.types.State;
41 import org.openhab.core.types.UnDefType;
42 import org.osgi.service.cm.ConfigurationAdmin;
43 import org.slf4j.Logger;
44 import org.slf4j.LoggerFactory;
47 * {@link ShellyManagerOtaPage} implements the Shelly Manager's device overview page
49 * @author Markus Michels - Initial contribution
52 public class ShellyManagerOverviewPage extends ShellyManagerPage {
53 private final Logger logger = LoggerFactory.getLogger(ShellyManagerOverviewPage.class);
55 public ShellyManagerOverviewPage(ConfigurationAdmin configurationAdmin,
56 ShellyTranslationProvider translationProvider, HttpClient httpClient, String localIp, int localPort,
57 ShellyHandlerFactory handlerFactory) {
58 super(configurationAdmin, translationProvider, httpClient, localIp, localPort, handlerFactory);
62 public ShellyMgrResponse generateContent(String path, Map<String, String[]> parameters) throws ShellyApiException {
63 String filter = getUrlParm(parameters, URLPARM_FILTER).toLowerCase();
64 String action = getUrlParm(parameters, URLPARM_ACTION).toLowerCase();
65 String uidParm = getUrlParm(parameters, URLPARM_UID).toLowerCase();
67 logger.debug("Generating overview for {} devices", getThingHandlers().size());
70 Map<String, String> properties = new HashMap<>();
71 properties.put(ATTRIBUTE_METATAG, "<meta http-equiv=\"refresh\" content=\"60\" />");
72 properties.put(ATTRIBUTE_CSS_HEADER, loadHTML(OVERVIEW_HEADER, properties));
74 String deviceHtml = "";
75 TreeMap<String, ShellyManagerInterface> sortedMap = new TreeMap<>();
76 for (Map.Entry<String, ShellyManagerInterface> th : getThingHandlers().entrySet()) { // sort by Device Name
77 ShellyManagerInterface handler = th.getValue();
78 sortedMap.put(getDisplayName(handler.getThing().getProperties()), handler);
81 html = loadHTML(HEADER_HTML, properties);
82 html += loadHTML(OVERVIEW_HTML, properties);
84 int filteredDevices = 0;
85 for (Map.Entry<String, ShellyManagerInterface> handler : sortedMap.entrySet()) {
87 ShellyManagerInterface th = handler.getValue();
88 ThingStatus status = th.getThing().getStatus();
89 ShellyDeviceProfile profile = th.getProfile();
90 String uid = getString(th.getThing().getUID().getAsString()); // handler.getKey();
92 if (action.equals(ACTION_REFRESH) && (uidParm.isEmpty() || uidParm.equals(uid))) {
93 // Refresh thing status, this is asynchronosly and takes 0-3sec
94 th.requestUpdates(1, true);
95 } else if (action.equals(ACTION_RES_STATS) && (uidParm.isEmpty() || uidParm.equals(uid))) {
97 } else if (action.equals(ACTION_OTACHECK) && (uidParm.isEmpty() || uidParm.equals(uid))) {
101 Map<String, String> warnings = getStatusWarnings(th);
102 if (applyFilter(th, filter) || (filter.equals(FILTER_ATTENTION) && !warnings.isEmpty())) {
105 fillProperties(properties, uid, handler.getValue());
106 String deviceType = getDeviceType(properties);
108 properties.put(ATTRIBUTE_DISPLAY_NAME, getDisplayName(properties));
109 properties.put(ATTRIBUTE_DEV_STATUS, fillDeviceStatus(warnings));
110 if (!warnings.isEmpty() && (status != ThingStatus.UNKNOWN)) {
111 properties.put(ATTRIBUTE_STATUS_ICON, ICON_ATTENTION);
113 if (!"unknown".equalsIgnoreCase(deviceType) && (status == ThingStatus.ONLINE)) {
114 properties.put(ATTRIBUTE_FIRMWARE_SEL, fillFirmwareHtml(uid, deviceType, profile.mode));
115 properties.put(ATTRIBUTE_ACTION_LIST, fillActionHtml(th, uid));
117 properties.put(ATTRIBUTE_FIRMWARE_SEL, "");
118 properties.put(ATTRIBUTE_ACTION_LIST, "");
120 html += loadHTML(OVERVIEW_DEVICE, properties);
122 } catch (ShellyApiException e) {
123 logger.debug("{}: Exception", LOG_PREFIX, e);
128 properties.put("numberDevices", "<span class=\"footerDevices\">" + "Number of devices: " + filteredDevices
129 + " of " + String.valueOf(getThingHandlers().size()) + " </span>");
130 properties.put(ATTRIBUTE_CSS_FOOTER, loadHTML(OVERVIEW_FOOTER, properties));
131 html += deviceHtml + loadHTML(FOOTER_HTML, properties);
132 return new ShellyMgrResponse(fillAttributes(html, properties), HttpStatus.OK_200);
135 private String fillFirmwareHtml(String uid, String deviceType, String mode) throws ShellyApiException {
136 String html = "\n\t\t\t\t<select name=\"fwList\" id=\"fwList\" onchange=\"location = this.options[this.selectedIndex].value;\">\n";
137 html += "\t\t\t\t\t<option value=\"\" selected disabled hidden>update to</option>\n";
139 String pVersion = "";
140 String bVersion = "";
141 String updateUrl = SHELLY_MGR_FWUPDATE_URI + "?" + URLPARM_UID + "=" + urlEncode(uid);
143 // Get current prod + beta version from original firmware repo
144 logger.debug("{}: Load firmware version list for device type {}", LOG_PREFIX, deviceType);
145 FwRepoEntry fw = getFirmwareRepoEntry(deviceType, mode);
146 pVersion = extractFwVersion(fw.version);
147 if (!pVersion.isEmpty()) {
148 html += "\t\t\t\t\t<option value=\"" + updateUrl + "&" + URLPARM_VERSION + "=" + FWPROD + "\">Release "
149 + pVersion + "</option>\n";
151 bVersion = extractFwVersion(fw.betaVer);
152 if (!bVersion.isEmpty()) {
153 html += "\t\t\t\t\t<option value=\"" + updateUrl + "&" + URLPARM_VERSION + "=" + FWBETA + "\">Beta "
154 + bVersion + "</option>\n";
157 // Add those from Shelly Firmware Archive
158 String json = httpGet(FWREPO_ARCH_URL + "?" + URLPARM_TYPE + "=" + deviceType);
159 if (json.startsWith("[]")) {
160 // no files available for this device type
161 logger.debug("{}: No firmware files found for device type {}", LOG_PREFIX, deviceType);
163 // Create selection list
164 json = "{" + json.replace("[{", "\"versions\":[{") + "}"; // make it an named array
165 FwArchList list = getFirmwareArchiveList(deviceType);
166 ArrayList<FwArchEntry> versions = list.versions;
167 if (versions != null) {
168 html += "\t\t\t\t\t<option value=\"\" disabled>-- Archive:</option>\n";
169 for (int i = versions.size() - 1; i >= 0; i--) {
170 FwArchEntry e = versions.get(i);
171 String version = getString(e.version);
172 ShellyVersionDTO v = new ShellyVersionDTO();
173 if (!version.equalsIgnoreCase(pVersion) && !version.equalsIgnoreCase(bVersion)
174 && (v.compare(version, SHELLY_API_MIN_FWCOIOT) >= 0) || version.contains("master")) {
175 html += "\t\t\t\t\t<option value=\"" + updateUrl + "&" + URLPARM_VERSION + "=" + version
176 + "\">" + version + "</option>\n";
181 } catch (ShellyApiException e) {
182 logger.debug("{}: Unable to retrieve firmware list: {}", LOG_PREFIX, e.toString());
185 html += "\t\t\t\t\t<option class=\"select-hr\" value=\"" + SHELLY_MGR_FWUPDATE_URI + "?uid=" + uid
186 + "&connection=custom\">Custom URL</option>\n";
188 html += "\t\t\t\t</select>\n\t\t\t";
193 private String fillActionHtml(ShellyManagerInterface handler, String uid) {
194 String html = "\n\t\t\t\t<select name=\"actionList\" id=\"actionList\" onchange=\"location = '"
195 + SHELLY_MGR_ACTION_URI + "?uid=" + urlEncode(uid) + "&" + URLPARM_ACTION
196 + "='+this.options[this.selectedIndex].value;\">\n";
197 html += "\t\t\t\t\t<option value=\"\" selected disabled>select</option>\n";
199 Map<String, String> actionList = ShellyManagerActionPage.getActions(handler.getProfile());
200 for (Map.Entry<String, String> a : actionList.entrySet()) {
201 String value = a.getValue();
202 String seperator = "";
203 if (value.startsWith("-")) {
204 // seperator = "class=\"select-hr\" ";
205 html += "\t\t\t\t\t<option class=\"select-hr\" role=\"seperator\" disabled> </option>\n";
206 value = substringAfterLast(value, "-");
208 html += "\t\t\t\t\t<option " + seperator + "value=\"" + a.getKey()
209 + (value.startsWith(ACTION_NONE) ? " disabled " : "") + "\">" + value + "</option>\n";
211 html += "\t\t\t\t</select>\n\t\t\t";
215 private boolean applyFilter(ShellyManagerInterface handler, String filter) {
216 ThingStatus status = handler.getThing().getStatus();
217 ShellyDeviceProfile profile = handler.getProfile();
221 return status == ThingStatus.ONLINE;
222 case FILTER_INACTIVE:
223 return status != ThingStatus.ONLINE;
224 case FILTER_ATTENTION:
227 // return handler.getChannelValue(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_UPDATE) == OnOffType.ON;
228 return getBool(profile.status.hasUpdate);
229 case FILTER_UNPROTECTED:
230 return !profile.auth;
237 private Map<String, String> getStatusWarnings(ShellyManagerInterface handler) {
238 Thing thing = handler.getThing();
239 ThingStatus status = handler.getThing().getStatus();
240 ShellyDeviceStats stats = handler.getStats();
241 ShellyDeviceProfile profile = handler.getProfile();
242 ShellyThingConfiguration config = thing.getConfiguration().as(ShellyThingConfiguration.class);
243 TreeMap<String, String> result = new TreeMap<>();
245 if ((status != ThingStatus.ONLINE) && (status != ThingStatus.UNKNOWN)) {
246 result.put("Thing Status", status.toString());
248 State wifiSignal = handler.getChannelValue(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_RSSI);
249 if ((profile.alwaysOn || (profile.hasBattery && (status == ThingStatus.ONLINE)))
250 && ((wifiSignal != UnDefType.NULL) && (((DecimalType) wifiSignal).intValue() < 2))) {
251 result.put("Weak WiFi Signal", wifiSignal.toString());
253 if (profile.hasBattery) {
254 State lowBattery = handler.getChannelValue(CHANNEL_GROUP_BATTERY, CHANNEL_SENSOR_BAT_LOW);
255 if ((lowBattery == OnOffType.ON)) {
256 lowBattery = handler.getChannelValue(CHANNEL_GROUP_BATTERY, CHANNEL_SENSOR_BAT_LEVEL);
257 result.put("Battery Low", lowBattery.toString());
261 if (stats.lastAlarm.equalsIgnoreCase(ALARM_TYPE_RESTARTED)) {
262 result.put("Device Alarm", ALARM_TYPE_RESTARTED + " (" + convertTimestamp(stats.lastAlarmTs) + ")");
264 if (getBool(profile.status.overtemperature)) {
265 result.put("Device Alarm", ALARM_TYPE_OVERTEMP);
267 if (getBool(profile.status.overload)) {
268 result.put("Device Alarm", ALARM_TYPE_OVERLOAD);
270 if (getBool(profile.status.loaderror)) {
271 result.put("Device Alarm", ALARM_TYPE_LOADERR);
273 if (profile.isSensor) {
274 State sensorError = handler.getChannelValue(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_ERROR);
275 if (sensorError != UnDefType.NULL) {
276 if (!sensorError.toString().isEmpty()) {
277 result.put("Device Alarm", ALARM_TYPE_SENSOR_ERROR);
281 if (profile.alwaysOn && (status == ThingStatus.ONLINE)) {
282 if ((config.eventsCoIoT) && (profile.settings.coiot != null)) {
283 if ((profile.settings.coiot.enabled != null) && !profile.settings.coiot.enabled) {
284 result.put("CoIoT Status", "COIOT_DISABLED");
285 } else if (stats.coiotMessages == 0) {
286 result.put("CoIoT Discovery", "NO_COIOT_DISCOVERY");
287 } else if (stats.coiotMessages < 2) {
288 result.put("CoIoT Multicast", "NO_COIOT_MULTICAST");
296 private String fillDeviceStatus(Map<String, String> devStatus) {
297 if (devStatus.isEmpty()) {
301 String result = "\t\t\t\t<tr><td colspan = \"2\">Notifications:</td></tr>";
302 for (Map.Entry<String, String> ds : devStatus.entrySet()) {
303 result += "\t\t\t\t<tr><td>" + ds.getKey() + "</td><td>" + ds.getValue() + "</td></tr>\n";