2 * Copyright (c) 2010-2021 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.manager.ShellyManagerConstants.*;
17 import static org.openhab.binding.shelly.internal.util.ShellyUtils.*;
18 import static org.openhab.core.thing.Thing.*;
20 import java.io.BufferedReader;
21 import java.io.IOException;
22 import java.io.InputStream;
23 import java.io.InputStreamReader;
24 import java.util.ArrayList;
25 import java.util.HashMap;
27 import java.util.Timer;
28 import java.util.TimerTask;
29 import java.util.TreeMap;
30 import java.util.concurrent.ExecutionException;
31 import java.util.concurrent.TimeUnit;
32 import java.util.concurrent.TimeoutException;
33 import java.util.stream.Collectors;
35 import org.eclipse.jdt.annotation.NonNullByDefault;
36 import org.eclipse.jdt.annotation.Nullable;
37 import org.eclipse.jetty.client.HttpClient;
38 import org.eclipse.jetty.client.api.ContentResponse;
39 import org.eclipse.jetty.client.api.Request;
40 import org.eclipse.jetty.http.HttpHeader;
41 import org.eclipse.jetty.http.HttpMethod;
42 import org.eclipse.jetty.http.HttpStatus;
43 import org.openhab.binding.shelly.internal.ShellyHandlerFactory;
44 import org.openhab.binding.shelly.internal.api.ShellyApiException;
45 import org.openhab.binding.shelly.internal.api.ShellyApiResult;
46 import org.openhab.binding.shelly.internal.api.ShellyDeviceProfile;
47 import org.openhab.binding.shelly.internal.api.ShellyHttpApi;
48 import org.openhab.binding.shelly.internal.config.ShellyBindingConfiguration;
49 import org.openhab.binding.shelly.internal.config.ShellyThingConfiguration;
50 import org.openhab.binding.shelly.internal.handler.ShellyDeviceStats;
51 import org.openhab.binding.shelly.internal.handler.ShellyManagerInterface;
52 import org.openhab.binding.shelly.internal.provider.ShellyTranslationProvider;
53 import org.openhab.core.library.types.DateTimeType;
54 import org.openhab.core.library.types.OnOffType;
55 import org.openhab.core.thing.Thing;
56 import org.openhab.core.thing.ThingStatus;
57 import org.openhab.core.thing.ThingStatusDetail;
58 import org.openhab.core.types.State;
59 import org.openhab.core.types.UnDefType;
60 import org.osgi.service.cm.Configuration;
61 import org.osgi.service.cm.ConfigurationAdmin;
62 import org.slf4j.Logger;
63 import org.slf4j.LoggerFactory;
65 import com.google.gson.Gson;
66 import com.google.gson.annotations.SerializedName;
69 * {@link ShellyManagerOtaPage} implements the Shelly Manager's page template
71 * @author Markus Michels - Initial contribution
74 public class ShellyManagerPage {
75 private final Logger logger = LoggerFactory.getLogger(ShellyManagerPage.class);
76 protected final ShellyTranslationProvider resources;
78 private final ShellyHandlerFactory handlerFactory;
79 protected final HttpClient httpClient;
80 protected final ConfigurationAdmin configurationAdmin;
81 protected final ShellyBindingConfiguration bindingConfig = new ShellyBindingConfiguration();
82 protected final String localIp;
83 protected final int localPort;
85 protected final Map<String, String> htmlTemplates = new HashMap<>();
86 protected final Gson gson = new Gson();
88 protected final ShellyManagerCache<String, FwRepoEntry> firmwareRepo = new ShellyManagerCache<>(15 * 60 * 1000);
89 protected final ShellyManagerCache<String, FwArchList> firmwareArch = new ShellyManagerCache<>(15 * 60 * 1000);
91 public static class ShellyMgrResponse {
92 public @Nullable Object data = "";
93 public String mimeType = "";
94 public String redirectUrl = "";
96 public Map<String, String> headers = new HashMap<>();
98 public ShellyMgrResponse() {
99 init("", HttpStatus.OK_200, "text/html", null);
102 public ShellyMgrResponse(Object data, int code) {
103 init(data, code, "text/html", null);
106 public ShellyMgrResponse(Object data, int code, String mimeType) {
107 init(data, code, mimeType, null);
110 public ShellyMgrResponse(Object data, int code, String mimeType, Map<String, String> headers) {
111 init(data, code, mimeType, headers);
114 private void init(Object message, int code, String mimeType, @Nullable Map<String, String> headers) {
117 this.mimeType = mimeType;
118 this.headers = headers != null ? headers : new TreeMap<>();
121 public void setRedirect(String redirectUrl) {
122 this.redirectUrl = redirectUrl;
126 public static class FwArchEntry {
127 // {"version":"v1.5.10","file":"SHSW-1.zip"}
128 public @Nullable String version;
129 public @Nullable String file;
132 public static class FwArchList {
133 public @Nullable ArrayList<FwArchEntry> versions;
136 public static class FwRepoEntry {
137 public @Nullable String url; // prod
138 public @Nullable String version;
140 @SerializedName("beta_url")
141 public @Nullable String betaUrl; // beta version if avilable
142 @SerializedName("beta_ver")
143 public @Nullable String betaVer;
146 public ShellyManagerPage(ConfigurationAdmin configurationAdmin, ShellyTranslationProvider translationProvider,
147 HttpClient httpClient, String localIp, int localPort, ShellyHandlerFactory handlerFactory) {
148 this.configurationAdmin = configurationAdmin;
149 this.resources = translationProvider;
150 this.handlerFactory = handlerFactory;
151 this.httpClient = httpClient;
152 this.localIp = localIp;
153 this.localPort = localPort;
156 public ShellyMgrResponse generateContent(String path, Map<String, String[]> parameters) throws ShellyApiException {
157 return new ShellyMgrResponse("Invalid Request", HttpStatus.BAD_REQUEST_400);
160 protected String loadHTML(String template) throws ShellyApiException {
161 if (htmlTemplates.containsKey(template)) {
162 return getString(htmlTemplates.get(template));
166 String file = TEMPLATE_PATH + template;
167 logger.debug("Read HTML from {}", file);
168 ClassLoader cl = ShellyManagerInterface.class.getClassLoader();
170 try (InputStream inputStream = cl.getResourceAsStream(file)) {
171 if (inputStream != null) {
172 html = new BufferedReader(new InputStreamReader(inputStream)).lines()
173 .collect(Collectors.joining("\n"));
174 htmlTemplates.put(template, html);
176 } catch (IOException e) {
177 throw new ShellyApiException("Unable to read " + file + " from bundle resources!", e);
183 protected String loadHTML(String template, Map<String, String> properties) throws ShellyApiException {
184 properties.put(ATTRIBUTE_URI, SHELLY_MANAGER_URI);
185 String html = loadHTML(template);
186 return fillAttributes(html, properties);
189 protected Map<String, String> fillProperties(Map<String, String> properties, String uid,
190 ShellyManagerInterface th) {
192 Configuration serviceConfig = configurationAdmin.getConfiguration("binding." + BINDING_ID);
193 bindingConfig.updateFromProperties(serviceConfig.getProperties());
194 } catch (IOException e) {
195 logger.debug("ShellyManager: Unable to get bindingConfig");
198 properties.putAll(th.getThing().getProperties());
200 Thing thing = th.getThing();
201 ThingStatus status = thing.getStatus();
202 properties.put("thingName", getString(thing.getLabel()));
203 properties.put("thingStatus", status.toString());
204 ThingStatusDetail detail = thing.getStatusInfo().getStatusDetail();
205 properties.put("thingStatusDetail", detail.equals(ThingStatusDetail.NONE) ? "" : getString(detail.toString()));
206 properties.put("thingStatusDescr", getString(thing.getStatusInfo().getDescription()));
207 properties.put(ATTRIBUTE_UID, uid);
209 ShellyDeviceProfile profile = th.getProfile();
210 ShellyThingConfiguration config = thing.getConfiguration().as(ShellyThingConfiguration.class);
211 ShellyDeviceStats stats = th.getStats();
212 properties.putAll(stats.asProperties());
214 for (Map.Entry<String, Object> p : thing.getConfiguration().getProperties().entrySet()) {
215 String key = p.getKey();
216 if (p.getValue() != null) {
217 String value = p.getValue().toString();
218 properties.put(key, value);
222 State state = th.getChannelValue(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_NAME);
223 if (state != UnDefType.NULL) {
224 addAttribute(properties, th, CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_NAME);
226 // If the Shelly doesn't provide a device name (not configured) we use the service name
227 String deviceName = getDeviceName(properties);
228 properties.put(PROPERTY_DEV_NAME,
229 !deviceName.isEmpty() ? deviceName : getString(properties.get(PROPERTY_SERVICE_NAME)));
232 if (config.userId.isEmpty()) {
233 // Get defauls from Binding Config
234 properties.put("userId", bindingConfig.defaultUserId);
235 properties.put("password", bindingConfig.defaultPassword);
238 addAttribute(properties, th, CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_RSSI);
239 addAttribute(properties, th, CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_UPTIME);
240 addAttribute(properties, th, CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_HEARTBEAT);
241 addAttribute(properties, th, CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_ITEMP);
242 addAttribute(properties, th, CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_WAKEUP);
243 addAttribute(properties, th, CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_CHARGER);
244 addAttribute(properties, th, CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_UPDATE);
245 addAttribute(properties, th, CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_ALARM);
246 addAttribute(properties, th, CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_CHARGER);
248 properties.put(ATTRIBUTE_DEBUG_MODE, getOption(profile.settings.debugEnable));
249 properties.put(ATTRIBUTE_DISCOVERABLE, String.valueOf(getBool(profile.settings.discoverable)));
250 properties.put(ATTRIBUTE_WIFI_RECOVERY, String.valueOf(getBool(profile.settings.wifiRecoveryReboot)));
251 properties.put(ATTRIBUTE_APR_MODE,
252 profile.settings.apRoaming != null ? getOption(profile.settings.apRoaming.enabled) : "n/a");
253 properties.put(ATTRIBUTE_APR_TRESHOLD,
254 profile.settings.apRoaming != null ? getOption(profile.settings.apRoaming.threshold) : "n/a");
255 properties.put(ATTRIBUTE_PWD_PROTECT,
256 profile.auth ? "enabled, user=" + getString(profile.settings.login.username) : "disabled");
257 String tz = getString(profile.settings.timezone);
258 properties.put(ATTRIBUTE_TIMEZONE,
259 (tz.isEmpty() ? "n/a" : tz) + ", auto-detect: " + getBool(profile.settings.tzautodetect));
260 properties.put(ATTRIBUTE_ACTIONS_SKIPPED,
261 profile.status.astats != null ? String.valueOf(profile.status.astats.skipped) : "n/a");
262 properties.put(ATTRIBUTE_MAX_ITEMP, stats.maxInternalTemp > 0 ? stats.maxInternalTemp + " °C" : "n/a");
264 // Shelly H&T: When external power is connected the battery level is not valid
265 if (!profile.isHT || (getInteger(profile.settings.externalPower) == 0)) {
266 addAttribute(properties, th, CHANNEL_GROUP_BATTERY, CHANNEL_SENSOR_BAT_LEVEL);
268 properties.put(CHANNEL_SENSOR_BAT_LEVEL, "USB");
271 String wiFiSignal = getString(properties.get(CHANNEL_DEVST_RSSI));
272 if (!wiFiSignal.isEmpty()) {
273 properties.put("wifiSignalRssi", wiFiSignal + " / " + stats.wifiRssi + " dBm");
274 properties.put("imgWiFi", "imgWiFi" + wiFiSignal);
277 if (profile.settings.sntp != null) {
278 properties.put(ATTRIBUTE_SNTP_SERVER,
279 getString(profile.settings.sntp.server) + ", enabled: " + getBool((profile.settings.sntp.enabled)));
282 boolean coiotEnabled = true;
283 if ((profile.settings.coiot != null) && (profile.settings.coiot.enabled != null)) {
284 coiotEnabled = profile.settings.coiot.enabled;
286 properties.put(ATTRIBUTE_COIOT_STATUS,
287 !coiotEnabled ? "Disbaled in settings" : "Events are " + (config.eventsCoIoT ? "enabled" : "disabled"));
288 properties.put(ATTRIBUTE_COIOT_PEER,
289 (profile.settings.coiot != null) && !getString(profile.settings.coiot.peer).isEmpty()
290 ? profile.settings.coiot.peer
292 if (profile.status.cloud != null) {
293 properties.put(ATTRIBUTE_CLOUD_STATUS,
294 getBool(profile.settings.cloud.enabled)
295 ? getBool(profile.status.cloud.connected) ? "connected" : "enabled"
298 properties.put(ATTRIBUTE_CLOUD_STATUS, "unknown");
300 if (profile.status.mqtt != null) {
301 properties.put(ATTRIBUTE_MQTT_STATUS,
302 getBool(profile.settings.mqtt.enable)
303 ? getBool(profile.status.mqtt.connected) ? "connected" : "enabled"
306 properties.put(ATTRIBUTE_MQTT_STATUS, "unknown");
309 String statusIcon = "";
310 ThingStatus ts = th.getThing().getStatus();
315 statusIcon = ICON_UNINITIALIZED;
318 ThingStatusDetail sd = th.getThing().getStatusInfo().getStatusDetail();
319 if (uid.contains(THING_TYPE_SHELLYUNKNOWN_STR) || (sd == ThingStatusDetail.CONFIGURATION_ERROR)
320 || (sd == ThingStatusDetail.HANDLER_CONFIGURATION_PENDING)) {
321 statusIcon = ICON_CONFIG;
325 statusIcon = ts.toString();
327 properties.put(ATTRIBUTE_STATUS_ICON, statusIcon.toLowerCase());
332 private void addAttribute(Map<String, String> properties, ShellyManagerInterface thingHandler, String group,
334 State state = thingHandler.getChannelValue(group, attribute);
336 if (state != UnDefType.NULL) {
337 if (state instanceof DateTimeType) {
338 DateTimeType dt = (DateTimeType) state;
340 case ATTRIBUTE_LAST_ALARM:
341 value = dt.format(null).replace('T', ' ').replace('-', '/');
344 value = getTimestamp(dt);
345 value = dt.format(null).replace('T', ' ').replace('-', '/');
348 value = state.toString();
351 properties.put(attribute, value);
354 protected String fillAttributes(String template, Map<String, String> properties) {
355 if (!template.contains("${")) {
356 // no replacement necessary
360 String result = template;
361 for (Map.Entry<String, String> var : properties.entrySet()) {
362 result = result.replaceAll(java.util.regex.Pattern.quote("${" + var.getKey() + "}"),
363 getValue(properties, var.getKey()));
366 if (result.contains("${")) {
367 return result.replaceAll("\\Q${\\E.*}", "");
373 protected String getValue(Map<String, String> properties, String attribute) {
374 String value = getString(properties.get(attribute));
375 if (!value.isEmpty()) {
377 case PROPERTY_FIRMWARE_VERSION:
378 value = substringBeforeLast(value, "-");
380 case PROPERTY_UPDATE_AVAILABLE:
381 value = value.replace(OnOffType.ON.toString(), "yes");
382 value = value.replace(OnOffType.OFF.toString(), "no");
384 case CHANNEL_DEVST_HEARTBEAT:
391 protected FwRepoEntry getFirmwareRepoEntry(String deviceType, String mode) throws ShellyApiException {
392 logger.debug("ShellyManager: Load firmware list from {}", FWREPO_PROD_URL);
393 FwRepoEntry fw = null;
394 if (firmwareRepo.containsKey(deviceType)) {
395 fw = firmwareRepo.get(deviceType);
397 String json = httpGet(FWREPO_PROD_URL); // returns a strange JSON format so we are parsing this manually
398 String entry = substringBetween(json, "\"" + deviceType + "\":{", "}");
399 if (!entry.isEmpty()) {
400 entry = "{" + entry + "}";
404 * "url":"http:\/\/repo.shelly.cloud\/firmware\/SHPLG-1.zip",
405 * "version":"20201228-092318\/v1.9.3@ad2bb4e3",
406 * "beta_url":"http:\/\/repo.shelly.cloud\/firmware\/rc\/SHPLG-1.zip",
407 * "beta_ver":"20201223-093703\/v1.9.3-rc5@3f583801"
410 fw = fromJson(gson, entry, FwRepoEntry.class);
412 // Special case: RGW2 has a split firmware - xxx-white.zip vs. xxx-color.zip
413 if (!mode.isEmpty() && deviceType.equalsIgnoreCase(SHELLYDT_RGBW2)) {
414 // check for spilt firmware
415 String url = substringBefore(fw.url, ".zip") + "-" + mode + ".zip";
418 logger.debug("ShellyManager: Release Split-URL for device type {} is {}", deviceType, url);
420 url = substringBefore(fw.betaUrl, ".zip") + "-" + mode + ".zip";
423 logger.debug("ShellyManager: Beta Split-URL for device type {} is {}", deviceType, url);
427 firmwareRepo.put(deviceType, fw);
430 return fw != null ? fw : new FwRepoEntry();
433 protected FwArchList getFirmwareArchiveList(String deviceType) throws ShellyApiException {
437 if (firmwareArch.contains(deviceType)) {
438 list = firmwareArch.get(deviceType); // return from cache
445 if (!deviceType.isEmpty()) {
446 json = httpGet(FWREPO_ARCH_URL + "?type=" + deviceType);
448 } catch (ShellyApiException e) {
449 logger.debug("{}: Unable to get firmware list for device type {}: {}", LOG_PREFIX, deviceType,
452 if (json.isEmpty() || json.startsWith("[]")) {
453 // no files available for this device type
454 logger.info("{}: No firmware files found for device type {}", LOG_PREFIX, deviceType);
455 list = new FwArchList();
456 list.versions = new ArrayList<FwArchEntry>();
458 // Create selection list
459 json = "{" + json.replace("[{", "\"versions\":[{") + "}"; // make it an named array
460 list = fromJson(gson, json, FwArchList.class);
463 // save list to cache
464 firmwareArch.put(deviceType, list);
468 protected boolean testUrl(String url) {
473 httpHeadl(url); // causes exception on 404
475 } catch (ShellyApiException e) {
480 protected String httpGet(String url) throws ShellyApiException {
481 return httpRequest(HttpMethod.GET, url);
484 protected String httpHeadl(String url) throws ShellyApiException {
485 return httpRequest(HttpMethod.HEAD, url);
488 protected String httpRequest(HttpMethod method, String url) throws ShellyApiException {
489 ShellyApiResult apiResult = new ShellyApiResult();
492 Request request = httpClient.newRequest(url).method(method).timeout(SHELLY_API_TIMEOUT_MS,
493 TimeUnit.MILLISECONDS);
494 request.header(HttpHeader.ACCEPT, ShellyHttpApi.CONTENT_TYPE_JSON);
495 logger.trace("{}: HTTP {} {}", LOG_PREFIX, method, url);
496 ContentResponse contentResponse = request.send();
497 apiResult = new ShellyApiResult(contentResponse);
498 String response = contentResponse.getContentAsString().replace("\t", "").replace("\r\n", "").trim();
499 logger.trace("{}: HTTP Response {}: {}", LOG_PREFIX, contentResponse.getStatus(), response);
501 // validate response, API errors are reported as Json
502 if (contentResponse.getStatus() != HttpStatus.OK_200) {
503 throw new ShellyApiException(apiResult);
506 } catch (ExecutionException | TimeoutException | InterruptedException | IllegalArgumentException e) {
507 throw new ShellyApiException("HTTP GET failed", e);
511 protected String getUrlParm(Map<String, String[]> parameters, String param) {
512 String[] p = parameters.get(param);
515 value = getString(p[0]);
520 protected String getMessage(String key, Object... arguments) {
521 return resources.get("manager." + key, arguments);
524 protected String getMessageP(String key, String msgClass, Object... arguments) {
525 return "<p class=\"" + msgClass + "\">" + getMessage(key, arguments) + "</p>\n";
528 protected String getMessageS(String key, String msgClass, Object... arguments) {
529 return "<span class=\"" + msgClass + "\">" + getMessage(key, arguments) + "</span>\n";
532 protected static String getDeviceType(Map<String, String> properties) {
533 return getString(properties.get(PROPERTY_MODEL_ID));
536 protected static String getDeviceIp(Map<String, String> properties) {
537 return getString(properties.get("deviceIp"));
540 protected static String getDeviceName(Map<String, String> properties) {
541 return getString(properties.get(PROPERTY_DEV_NAME));
544 protected static String getOption(@Nullable Boolean option) {
545 if (option == null) {
548 return option ? "enabled" : "disabled";
551 protected static String getOption(@Nullable Integer option) {
552 if (option == null) {
555 return option.toString();
558 protected static String getDisplayName(Map<String, String> properties) {
559 String name = getString(properties.get(PROPERTY_DEV_NAME));
560 if (name.isEmpty()) {
561 name = getString(properties.get(PROPERTY_SERVICE_NAME));
566 protected ShellyThingConfiguration getThingConfig(ShellyManagerInterface th, Map<String, String> properties) {
567 Thing thing = th.getThing();
568 ShellyThingConfiguration config = thing.getConfiguration().as(ShellyThingConfiguration.class);
569 if (config.userId.isEmpty()) {
570 config.userId = getString(properties.get("userId"));
571 config.password = getString(properties.get("password"));
576 protected void scheduleUpdate(ShellyManagerInterface th, String name, int delay) {
577 TimerTask task = new TimerTask() {
580 th.requestUpdates(1, true);
583 Timer timer = new Timer(name);
584 timer.schedule(task, delay * 1000);
587 protected Map<String, ShellyManagerInterface> getThingHandlers() {
588 return handlerFactory.getThingHandlers();
591 protected @Nullable ShellyManagerInterface getThingHandler(String uid) {
592 return getThingHandlers().get(uid);