2 * Copyright (c) 2010-2023 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(profile, uid, deviceType));
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 " + 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(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";
140 String pVersion = "";
141 String bVersion = "";
142 String updateUrl = SHELLY_MGR_FWUPDATE_URI + "?" + URLPARM_UID + "=" + urlEncode(uid);
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);
148 pVersion = extractFwVersion(fw.version);
149 bVersion = extractFwVersion(fw.betaVer);
151 pVersion = extractFwVersion(getString(profile.status.update.newVersion));
152 bVersion = extractFwVersion(getString(profile.status.update.betaVersion));
154 if (!pVersion.isEmpty()) {
155 html += "\t\t\t\t\t<option value=\"" + updateUrl + "&" + URLPARM_VERSION + "=" + FWPROD + "\">Release "
156 + pVersion + "</option>\n";
158 if (!bVersion.isEmpty()) {
159 html += "\t\t\t\t\t<option value=\"" + updateUrl + "&" + URLPARM_VERSION + "=" + FWBETA + "\">Beta "
160 + bVersion + "</option>\n";
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);
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";
192 ShellyApiException e) {
193 logger.debug("{}: Unable to retrieve firmware list: {}", LOG_PREFIX, e.toString());
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";
199 html += "\t\t\t\t</select>\n\t\t\t";
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";
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> </option>\n";
217 value = substringAfterLast(value, "-");
219 html += "\t\t\t\t\t<option " + seperator + "value=\"" + a.getKey()
220 + (value.startsWith(ACTION_NONE) ? " disabled " : "") + "\">" + value + "</option>\n";
222 html += "\t\t\t\t</select>\n\t\t\t";
226 private boolean applyFilter(ShellyManagerInterface handler, String filter) {
227 ThingStatus status = handler.getThing().getStatus();
228 ShellyDeviceProfile profile = handler.getProfile();
232 return status == ThingStatus.ONLINE;
233 case FILTER_INACTIVE:
234 return status != ThingStatus.ONLINE;
235 case FILTER_ATTENTION:
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;
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<>();
256 if ((status != ThingStatus.ONLINE) && (status != ThingStatus.UNKNOWN)) {
257 result.put("Thing Status", status.toString());
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());
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());
272 if (stats.lastAlarm.equalsIgnoreCase(ALARM_TYPE_RESTARTED)) {
273 result.put("Device Alarm", ALARM_TYPE_RESTARTED + " (" + convertTimestamp(stats.lastAlarmTs) + ")");
275 if (getBool(profile.status.overtemperature)) {
276 result.put("Device Alarm", ALARM_TYPE_OVERTEMP);
278 if (getBool(profile.status.overload)) {
279 result.put("Device Alarm", ALARM_TYPE_OVERLOAD);
281 if (getBool(profile.status.loaderror)) {
282 result.put("Device Alarm", ALARM_TYPE_LOADERR);
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);
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");
307 private String fillDeviceStatus(Map<String, String> devStatus) {
308 if (devStatus.isEmpty()) {
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";