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