]> git.basschouten.com Git - openhab-addons.git/blob
d7546543dc841b6d1f4116413cc260f4a68a5583
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2022 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.shelly.internal.manager;
14
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.*;
19
20 import java.util.ArrayList;
21 import java.util.HashMap;
22 import java.util.Map;
23 import java.util.TreeMap;
24
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;
45
46 /**
47  * {@link ShellyManagerOtaPage} implements the Shelly Manager's device overview page
48  *
49  * @author Markus Michels - Initial contribution
50  */
51 @NonNullByDefault
52 public class ShellyManagerOverviewPage extends ShellyManagerPage {
53     private final Logger logger = LoggerFactory.getLogger(ShellyManagerOverviewPage.class);
54
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);
59     }
60
61     @Override
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();
66
67         logger.debug("Generating overview for {} devices", getThingHandlers().size());
68
69         String html = "";
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));
73
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);
79         }
80
81         html = loadHTML(HEADER_HTML, properties);
82         html += loadHTML(OVERVIEW_HTML, properties);
83
84         int filteredDevices = 0;
85         for (Map.Entry<String, ShellyManagerInterface> handler : sortedMap.entrySet()) {
86             try {
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();
91
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))) {
96                     th.resetStats();
97                 } else if (action.equals(ACTION_OTACHECK) && (uidParm.isEmpty() || uidParm.equals(uid))) {
98                     th.resetStats();
99                 }
100
101                 Map<String, String> warnings = getStatusWarnings(th);
102                 if (applyFilter(th, filter) || (filter.equals(FILTER_ATTENTION) && !warnings.isEmpty())) {
103                     filteredDevices++;
104                     properties.clear();
105                     fillProperties(properties, uid, handler.getValue());
106                     String deviceType = getDeviceType(properties);
107
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);
112                     }
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));
116                     } else {
117                         properties.put(ATTRIBUTE_FIRMWARE_SEL, "");
118                         properties.put(ATTRIBUTE_ACTION_LIST, "");
119                     }
120                     html += loadHTML(OVERVIEW_DEVICE, properties);
121                 }
122             } catch (ShellyApiException e) {
123                 logger.debug("{}: Exception", LOG_PREFIX, e);
124             }
125         }
126
127         properties.clear();
128         properties.put("numberDevices", "<span class=\"footerDevices\">" + "Number of devices: " + filteredDevices
129                 + " of " + String.valueOf(getThingHandlers().size()) + "&nbsp;</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);
133     }
134
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";
138
139         String pVersion = "";
140         String bVersion = "";
141         String updateUrl = SHELLY_MGR_FWUPDATE_URI + "?" + URLPARM_UID + "=" + urlEncode(uid);
142         try {
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";
150             }
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";
155             }
156
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);
162             } else {
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";
177                         }
178                     }
179                 }
180             }
181         } catch (ShellyApiException e) {
182             logger.debug("{}: Unable to retrieve firmware list: {}", LOG_PREFIX, e.toString());
183         }
184
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";
187
188         html += "\t\t\t\t</select>\n\t\t\t";
189
190         return html;
191     }
192
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";
198
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>&nbsp;</option>\n";
206                 value = substringAfterLast(value, "-");
207             }
208             html += "\t\t\t\t\t<option " + seperator + "value=\"" + a.getKey()
209                     + (value.startsWith(ACTION_NONE) ? " disabled " : "") + "\">" + value + "</option>\n";
210         }
211         html += "\t\t\t\t</select>\n\t\t\t";
212         return html;
213     }
214
215     private boolean applyFilter(ShellyManagerInterface handler, String filter) {
216         ThingStatus status = handler.getThing().getStatus();
217         ShellyDeviceProfile profile = handler.getProfile();
218
219         switch (filter) {
220             case FILTER_ONLINE:
221                 return status == ThingStatus.ONLINE;
222             case FILTER_INACTIVE:
223                 return status != ThingStatus.ONLINE;
224             case FILTER_ATTENTION:
225                 return false;
226             case FILTER_UPDATE:
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;
231             case "*":
232             default:
233                 return true;
234         }
235     }
236
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<>();
244
245         if ((status != ThingStatus.ONLINE) && (status != ThingStatus.UNKNOWN)) {
246             result.put("Thing Status", status.toString());
247         }
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());
252         }
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());
258             }
259         }
260
261         if (stats.lastAlarm.equalsIgnoreCase(ALARM_TYPE_RESTARTED)) {
262             result.put("Device Alarm", ALARM_TYPE_RESTARTED + " (" + convertTimestamp(stats.lastAlarmTs) + ")");
263         }
264         if (getBool(profile.status.overtemperature)) {
265             result.put("Device Alarm", ALARM_TYPE_OVERTEMP);
266         }
267         if (getBool(profile.status.overload)) {
268             result.put("Device Alarm", ALARM_TYPE_OVERLOAD);
269         }
270         if (getBool(profile.status.loaderror)) {
271             result.put("Device Alarm", ALARM_TYPE_LOADERR);
272         }
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);
278                 }
279             }
280         }
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");
289                 }
290             }
291         }
292
293         return result;
294     }
295
296     private String fillDeviceStatus(Map<String, String> devStatus) {
297         if (devStatus.isEmpty()) {
298             return "";
299         }
300
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";
304         }
305         return result;
306     }
307 }