2 * Copyright (c) 2010-2024 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, "");
121 properties.put(ATTRIBUTE_DISPLAY_NAME, profile.thingName);
122 properties.put(ATTRIBUTE_DEVICEIP, "n/a");
123 properties.put(PROPERTY_WIFI_NETW, "Bluetooth");
125 html += loadHTML(OVERVIEW_DEVICE, properties);
127 } catch (ShellyApiException e) {
128 logger.debug("{}: Exception", LOG_PREFIX, e);
133 properties.put("numberDevices", "<span class=\"footerDevices\">" + "Number of devices: " + filteredDevices
134 + " of " + getThingHandlers().size() + " </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);
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";
145 String pVersion = "";
146 String bVersion = "";
147 String updateUrl = SHELLY_MGR_FWUPDATE_URI + "?" + URLPARM_UID + "=" + urlEncode(uid);
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);
153 pVersion = extractFwVersion(fw.version);
154 bVersion = extractFwVersion(fw.betaVer);
156 pVersion = extractFwVersion(getString(profile.status.update.newVersion));
157 bVersion = extractFwVersion(getString(profile.status.update.betaVersion));
159 if (!pVersion.isEmpty()) {
160 html += "\t\t\t\t\t<option value=\"" + updateUrl + "&" + URLPARM_VERSION + "=" + FWPROD + "\">Release "
161 + pVersion + "</option>\n";
163 if (!bVersion.isEmpty()) {
164 html += "\t\t\t\t\t<option value=\"" + updateUrl + "&" + URLPARM_VERSION + "=" + FWBETA + "\">Beta "
165 + bVersion + "</option>\n";
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);
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";
197 ShellyApiException e) {
198 logger.debug("{}: Unable to retrieve firmware list: {}", LOG_PREFIX, e.toString());
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";
205 html += "\">Check Device App";
207 html += "</option>\n\t\t\t\t</select>\n\t\t\t";
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";
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> </option>\n";
225 value = substringAfterLast(value, "-");
227 html += "\t\t\t\t\t<option " + seperator + "value=\"" + a.getKey()
228 + (value.startsWith(ACTION_NONE) ? " disabled " : "") + "\">" + value + "</option>\n";
230 html += "\t\t\t\t</select>\n\t\t\t";
234 private boolean applyFilter(ShellyManagerInterface handler, String filter) {
235 ThingStatus status = handler.getThing().getStatus();
236 ShellyDeviceProfile profile = handler.getProfile();
240 return status == ThingStatus.ONLINE;
241 case FILTER_INACTIVE:
242 return status != ThingStatus.ONLINE;
243 case FILTER_ATTENTION:
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;
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<>();
266 if ((status != ThingStatus.ONLINE) && (status != ThingStatus.UNKNOWN)) {
267 result.put("Thing Status", status.toString());
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());
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());
282 if (stats.lastAlarm.equalsIgnoreCase(ALARM_TYPE_RESTARTED)) {
283 result.put("Device Alarm", ALARM_TYPE_RESTARTED + " (" + convertTimestamp(stats.lastAlarmTs) + ")");
285 if (getBool(profile.status.overtemperature)) {
286 result.put("Device Alarm", ALARM_TYPE_OVERTEMP);
288 if (getBool(profile.status.overload)) {
289 result.put("Device Alarm", ALARM_TYPE_OVERLOAD);
291 if (getBool(profile.status.loaderror)) {
292 result.put("Device Alarm", ALARM_TYPE_LOADERR);
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);
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");
317 private String fillDeviceStatus(Map<String, String> devStatus) {
318 if (devStatus.isEmpty()) {
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";