]> git.basschouten.com Git - openhab-addons.git/blob
ddc696e80543a8e9c30b8fad808abf7404f5598f
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 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(profile, uid, deviceType));
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 " + 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(ShellyDeviceProfile profile, String uid, String deviceType)
136             throws ShellyApiException {
137         String html = "\n\t\t\t\t<select name=\"fwList\" id=\"fwList\" onchange=\"location = this.options[this.selectedIndex].value;\">\n";
138         html += "\t\t\t\t\t<option value=\"\" selected disabled hidden>update to</option>\n";
139
140         String pVersion = "";
141         String bVersion = "";
142         String updateUrl = SHELLY_MGR_FWUPDATE_URI + "?" + URLPARM_UID + "=" + urlEncode(uid);
143         try {
144             if (!profile.isGen2) { // currently there is no public firmware repo for Gen2
145                 logger.debug("{}: Load firmware version list for device type {}", LOG_PREFIX, deviceType);
146                 FwRepoEntry fw = getFirmwareRepoEntry(deviceType, profile.mode);
147
148                 pVersion = extractFwVersion(fw.version);
149                 bVersion = extractFwVersion(fw.betaVer);
150             } else {
151                 pVersion = extractFwVersion(getString(profile.status.update.newVersion));
152                 bVersion = extractFwVersion(getString(profile.status.update.betaVersion));
153             }
154             if (!pVersion.isEmpty()) {
155                 html += "\t\t\t\t\t<option value=\"" + updateUrl + "&" + URLPARM_VERSION + "=" + FWPROD + "\">Release "
156                         + pVersion + "</option>\n";
157             }
158             if (!bVersion.isEmpty()) {
159                 html += "\t\t\t\t\t<option value=\"" + updateUrl + "&" + URLPARM_VERSION + "=" + FWBETA + "\">Beta "
160                         + bVersion + "</option>\n";
161             }
162
163             if (!profile.isGen2) { // currently no online repo for Gen2
164                 // Add those from Shelly Firmware Archive
165                 String json = httpGet(FWREPO_ARCH_URL + "?" + URLPARM_TYPE + "=" + deviceType);
166                 if (json.startsWith("[]")) {
167                     // no files available for this device type
168                     logger.debug("{}: No firmware files found for device type {}", LOG_PREFIX, deviceType);
169                 } else {
170                     // Create selection list
171                     json = "{" + json.replace("[{", "\"versions\":[{") + "}"; // make it a named array
172                     FwArchList list = getFirmwareArchiveList(deviceType);
173                     ArrayList<FwArchEntry> versions = list.versions;
174                     if (versions != null) {
175                         html += "\t\t\t\t\t<option value=\"\" disabled>-- Archive:</option>\n";
176                         for (int i = versions.size() - 1; i >= 0; i--) {
177                             FwArchEntry e = versions.get(i);
178                             String version = getString(e.version);
179                             ShellyVersionDTO v = new ShellyVersionDTO();
180                             if (!version.equalsIgnoreCase(pVersion) && !version.equalsIgnoreCase(bVersion)
181                                     && (v.compare(version, SHELLY_API_MIN_FWCOIOT) >= 0)
182                                     || version.contains("master")) {
183                                 html += "\t\t\t\t\t<option value=\"" + updateUrl + "&" + URLPARM_VERSION + "=" + version
184                                         + "\">" + version + "</option>\n";
185                             }
186                         }
187                     }
188                 }
189             }
190         } catch (
191
192         ShellyApiException e) {
193             logger.debug("{}: Unable to retrieve firmware list: {}", LOG_PREFIX, e.toString());
194         }
195
196         html += "\t\t\t\t\t<option class=\"select-hr\" value=\"" + SHELLY_MGR_FWUPDATE_URI + "?uid=" + uid
197                 + "&connection=custom\">Custom URL</option>\n";
198
199         html += "\t\t\t\t</select>\n\t\t\t";
200
201         return html;
202     }
203
204     private String fillActionHtml(ShellyManagerInterface handler, String uid) {
205         String html = "\n\t\t\t\t<select name=\"actionList\" id=\"actionList\" onchange=\"location = '"
206                 + SHELLY_MGR_ACTION_URI + "?uid=" + urlEncode(uid) + "&" + URLPARM_ACTION
207                 + "='+this.options[this.selectedIndex].value;\">\n";
208         html += "\t\t\t\t\t<option value=\"\" selected disabled>select</option>\n";
209
210         Map<String, String> actionList = ShellyManagerActionPage.getActions(handler.getProfile());
211         for (Map.Entry<String, String> a : actionList.entrySet()) {
212             String value = a.getValue();
213             String seperator = "";
214             if (value.startsWith("-")) {
215                 // seperator = "class=\"select-hr\" ";
216                 html += "\t\t\t\t\t<option class=\"select-hr\" role=\"seperator\" disabled>&nbsp;</option>\n";
217                 value = substringAfterLast(value, "-");
218             }
219             html += "\t\t\t\t\t<option " + seperator + "value=\"" + a.getKey()
220                     + (value.startsWith(ACTION_NONE) ? " disabled " : "") + "\">" + value + "</option>\n";
221         }
222         html += "\t\t\t\t</select>\n\t\t\t";
223         return html;
224     }
225
226     private boolean applyFilter(ShellyManagerInterface handler, String filter) {
227         ThingStatus status = handler.getThing().getStatus();
228         ShellyDeviceProfile profile = handler.getProfile();
229
230         switch (filter) {
231             case FILTER_ONLINE:
232                 return status == ThingStatus.ONLINE;
233             case FILTER_INACTIVE:
234                 return status != ThingStatus.ONLINE;
235             case FILTER_ATTENTION:
236                 return false;
237             case FILTER_UPDATE:
238                 // return handler.getChannelValue(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_UPDATE) == OnOffType.ON;
239                 return getBool(profile.status.hasUpdate);
240             case FILTER_UNPROTECTED:
241                 return !profile.auth;
242             case "*":
243             default:
244                 return true;
245         }
246     }
247
248     private Map<String, String> getStatusWarnings(ShellyManagerInterface handler) {
249         Thing thing = handler.getThing();
250         ThingStatus status = handler.getThing().getStatus();
251         ShellyDeviceStats stats = handler.getStats();
252         ShellyDeviceProfile profile = handler.getProfile();
253         ShellyThingConfiguration config = thing.getConfiguration().as(ShellyThingConfiguration.class);
254         TreeMap<String, String> result = new TreeMap<>();
255
256         if ((status != ThingStatus.ONLINE) && (status != ThingStatus.UNKNOWN)) {
257             result.put("Thing Status", status.toString());
258         }
259         State wifiSignal = handler.getChannelValue(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_RSSI);
260         if ((profile.alwaysOn || (profile.hasBattery && (status == ThingStatus.ONLINE)))
261                 && ((wifiSignal != UnDefType.NULL) && (((DecimalType) wifiSignal).intValue() < 2))) {
262             result.put("Weak WiFi Signal", wifiSignal.toString());
263         }
264         if (profile.hasBattery) {
265             State lowBattery = handler.getChannelValue(CHANNEL_GROUP_BATTERY, CHANNEL_SENSOR_BAT_LOW);
266             if ((lowBattery == OnOffType.ON)) {
267                 lowBattery = handler.getChannelValue(CHANNEL_GROUP_BATTERY, CHANNEL_SENSOR_BAT_LEVEL);
268                 result.put("Battery Low", lowBattery.toString());
269             }
270         }
271
272         if (stats.lastAlarm.equalsIgnoreCase(ALARM_TYPE_RESTARTED)) {
273             result.put("Device Alarm", ALARM_TYPE_RESTARTED + " (" + convertTimestamp(stats.lastAlarmTs) + ")");
274         }
275         if (getBool(profile.status.overtemperature)) {
276             result.put("Device Alarm", ALARM_TYPE_OVERTEMP);
277         }
278         if (getBool(profile.status.overload)) {
279             result.put("Device Alarm", ALARM_TYPE_OVERLOAD);
280         }
281         if (getBool(profile.status.loaderror)) {
282             result.put("Device Alarm", ALARM_TYPE_LOADERR);
283         }
284         if (profile.isSensor) {
285             State sensorError = handler.getChannelValue(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_ERROR);
286             if (sensorError != UnDefType.NULL) {
287                 if (!sensorError.toString().isEmpty()) {
288                     result.put("Device Alarm", ALARM_TYPE_SENSOR_ERROR);
289                 }
290             }
291         }
292         if (profile.alwaysOn && (status == ThingStatus.ONLINE)) {
293             if ((config.eventsCoIoT) && (profile.settings.coiot != null)) {
294                 if ((profile.settings.coiot.enabled != null) && !profile.settings.coiot.enabled) {
295                     result.put("CoIoT Status", "COIOT_DISABLED");
296                 } else if (stats.protocolMessages == 0) {
297                     result.put("CoIoT Discovery", "NO_COIOT_DISCOVERY");
298                 } else if (stats.protocolMessages < 2) {
299                     result.put("CoIoT Multicast", "NO_COIOT_MULTICAST");
300                 }
301             }
302         }
303
304         return result;
305     }
306
307     private String fillDeviceStatus(Map<String, String> devStatus) {
308         if (devStatus.isEmpty()) {
309             return "";
310         }
311
312         String result = "\t\t\t\t<tr><td colspan = \"2\">Notifications:</td></tr>";
313         for (Map.Entry<String, String> ds : devStatus.entrySet()) {
314             result += "\t\t\t\t<tr><td>" + ds.getKey() + "</td><td>" + ds.getValue() + "</td></tr>\n";
315         }
316         return result;
317     }
318 }