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.PROPERTY_SERVICE_NAME;
16 import static org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.SHELLY_COIOT_MCAST;
17 import static org.openhab.binding.shelly.internal.manager.ShellyManagerConstants.*;
18 import static org.openhab.binding.shelly.internal.util.ShellyUtils.*;
20 import java.util.HashMap;
21 import java.util.LinkedHashMap;
24 import org.eclipse.jdt.annotation.NonNullByDefault;
25 import org.eclipse.jetty.client.HttpClient;
26 import org.eclipse.jetty.http.HttpStatus;
27 import org.openhab.binding.shelly.internal.ShellyHandlerFactory;
28 import org.openhab.binding.shelly.internal.api.ShellyApiException;
29 import org.openhab.binding.shelly.internal.api.ShellyApiInterface;
30 import org.openhab.binding.shelly.internal.api.ShellyDeviceProfile;
31 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyOtaCheckResult;
32 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsLogin;
33 import org.openhab.binding.shelly.internal.api1.Shelly1CoapJSonDTO;
34 import org.openhab.binding.shelly.internal.api1.Shelly1HttpApi;
35 import org.openhab.binding.shelly.internal.config.ShellyThingConfiguration;
36 import org.openhab.binding.shelly.internal.handler.ShellyManagerInterface;
37 import org.openhab.binding.shelly.internal.provider.ShellyTranslationProvider;
38 import org.openhab.core.thing.ThingStatusDetail;
39 import org.osgi.service.cm.ConfigurationAdmin;
40 import org.slf4j.Logger;
41 import org.slf4j.LoggerFactory;
44 * {@link ShellyManagerActionPage} implements the Shelly Manager's action page
46 * @author Markus Michels - Initial contribution
49 public class ShellyManagerActionPage extends ShellyManagerPage {
50 private final Logger logger = LoggerFactory.getLogger(ShellyManagerActionPage.class);
52 public ShellyManagerActionPage(ConfigurationAdmin configurationAdmin, ShellyTranslationProvider translationProvider,
53 HttpClient httpClient, String localIp, int localPort, ShellyHandlerFactory handlerFactory) {
54 super(configurationAdmin, translationProvider, httpClient, localIp, localPort, handlerFactory);
58 public ShellyMgrResponse generateContent(String path, Map<String, String[]> parameters) throws ShellyApiException {
59 String action = getUrlParm(parameters, URLPARM_ACTION);
60 String uid = getUrlParm(parameters, URLPARM_UID);
61 String update = getUrlParm(parameters, URLPARM_UPDATE);
62 if (uid.isEmpty() || action.isEmpty()) {
63 return new ShellyMgrResponse("Invalid URL parameters: " + parameters.toString(),
64 HttpStatus.BAD_REQUEST_400);
67 Map<String, String> properties = new HashMap<>();
68 properties.put(ATTRIBUTE_METATAG, "");
69 properties.put(ATTRIBUTE_CSS_HEADER, "");
70 properties.put(ATTRIBUTE_CSS_FOOTER, "");
71 String html = loadHTML(HEADER_HTML, properties);
73 ShellyManagerInterface th = getThingHandler(uid);
75 fillProperties(properties, uid, th);
77 Map<String, String> actions = getActions(th.getProfile());
78 String actionUrl = SHELLY_MGR_OVERVIEW_URI;
79 String actionButtonLabel = "OK"; // Default
80 String serviceName = getValue(properties, PROPERTY_SERVICE_NAME);
83 ShellyThingConfiguration config = getThingConfig(th, properties);
84 ShellyDeviceProfile profile = th.getProfile();
85 ShellyApiInterface api = th.getApi();
86 new Shelly1HttpApi(uid, config, httpClient);
90 case ACTION_RES_STATS:
92 message = getMessageP("action.resstats.confirm", MCINFO);
96 if ("yes".equalsIgnoreCase(update)) {
97 message = getMessageP("action.restart.info", MCINFO);
98 actionButtonLabel = "Ok";
99 new Thread(() -> { // schedule asynchronous reboot
102 } catch (ShellyApiException e) {
103 // maybe the device restarts before returning the http response
105 setRestarted(th, uid); // refresh after reboot
107 refreshTimer = profile.isMotion ? 60 : 30;
109 message = getMessageS("action.restart.confirm", MCINFO);
110 actionUrl = buildActionUrl(uid, action);
114 // Get device settings
115 if (config.userId.isEmpty() || config.password.isEmpty()) {
116 message = getMessageP("action.protect.id-missing", MCWARNING);
120 if (!"yes".equalsIgnoreCase(update)) {
121 ShellySettingsLogin status = api.getLoginSettings();
122 message = getMessage("action.protect.status", getBool(status.enabled) ? "enabled" : "disabled",
124 + getMessageP("action.protect.new", MCINFO, config.userId, config.password);
125 actionUrl = buildActionUrl(uid, action);
127 api.setLoginCredentials(config.userId, config.password);
128 message = getMessageP("action.protect.confirm", MCINFO, config.userId, config.password);
132 case ACTION_SETCOIOT_MCAST:
133 case ACTION_SETCOIOT_PEER:
134 if ((profile.settings.coiot == null) || (profile.settings.coiot.peer == null)) {
135 // feature not available
136 message = getMessage("coiot.mode-not-suppored", MCWARNING, action);
140 String peer = getString(profile.settings.coiot.peer);
141 boolean mcast = peer.isEmpty() || SHELLY_COIOT_MCAST.equalsIgnoreCase(peer) || profile.isMotion;
142 String newPeer = mcast ? localIp + ":" + Shelly1CoapJSonDTO.COIOT_PORT : SHELLY_COIOT_MCAST;
143 String displayPeer = mcast ? newPeer : "Multicast";
145 if (profile.isMotion && action.equalsIgnoreCase(ACTION_SETCOIOT_MCAST)) {
146 // feature not available
147 message = getMessageP("coiot.multicast-not-supported", "warning", displayPeer);
151 if (!"yes".equalsIgnoreCase(update)) {
152 message = getMessageP("coiot.current-peer", MCMESSAGE, mcast ? "Multicast" : peer)
153 + getMessageP("coiot.new-peer", MCINFO, displayPeer)
154 + getMessageP(mcast ? "coiot.mode-peer" : "coiot.mode-mcast", MCMESSAGE);
155 actionUrl = buildActionUrl(uid, action);
157 new Thread(() -> { // schedule asynchronous reboot
159 api.setCoIoTPeer(newPeer);
161 } catch (ShellyApiException e) {
162 // maybe the device restarts before returning the http response
164 setRestarted(th, uid); // refresh after reboot
167 // The device needs a restart after changing the peer mode
168 message = getMessageP("action.restart.info", MCINFO);
173 case ACTION_DISCLOUD:
174 boolean enabled = action.equals(ACTION_ENCLOUD);
175 api.setCloud(enabled);
176 message = getMessageP("action.setcloud.config", MCINFO, enabled ? "enabled" : "disabled");
180 if (!"yes".equalsIgnoreCase(update)) {
181 message = getMessageP("action.reset.warning", MCWARNING, serviceName);
182 actionUrl = buildActionUrl(uid, action);
184 new Thread(() -> { // schedule asynchronous reboot
187 setRestarted(th, uid);
188 } catch (ShellyApiException e) {
189 // maybe the device restarts before returning the http response
192 message = getMessageP("action.reset.confirm", MCINFO, serviceName);
196 case ACTION_OTACHECK:
198 ShellyOtaCheckResult result = api.checkForUpdate();
199 message = getMessage("action.checkupd." + result.status);
200 } catch (ShellyApiException e) {
201 // maybe the device restarts before returning the http response
202 message = getMessageP("action.checkupd.failed", e.toString());
207 case ACTION_DISDEBUG:
208 boolean enable = ACTION_ENDEBUG.equalsIgnoreCase(action);
209 if (!"yes".equalsIgnoreCase(update)) {
210 message = getMessage(enable ? "action.debug-enable" : "action.debug-disable");
211 actionUrl = buildActionUrl(uid, action);
213 new Thread(() -> { // schedule asynchronous reboot
215 api.setDebug(enable);
216 } catch (ShellyApiException e) {
217 // maybe the device restarts before returning the http response
221 message = getMessage("action.debug-confirm", enable ? "enabled" : "disabled");
226 if (!"yes".equalsIgnoreCase(update)) {
227 message = getMessage("action.resetsta-info");
228 actionUrl = buildActionUrl(uid, action);
232 message = getMessage("action.resetsta-confirm");
233 } catch (ShellyApiException e) {
234 message = getMessageP("action.resetsta-failed", e.toString());
239 case ACTION_ENWIFIREC:
240 case ACTION_DISWIFIREC:
241 enable = ACTION_ENWIFIREC.equalsIgnoreCase(action);
242 if (!"yes".equalsIgnoreCase(update)) {
243 message = getMessage(enable ? "action.setwifirec-enable" : "action.setwifirec-disable");
244 actionUrl = buildActionUrl(uid, action);
247 api.setWiFiRecovery(enable);
248 message = getMessage("action.setwifirec-confirm", enable ? "enabled" : "disabled");
249 } catch (ShellyApiException e) {
250 message = getMessage("action.setwifirec-failed", e.toString());
255 case ACTION_ENAPROAMING:
256 case ACTION_DISAPROAMING:
257 enable = ACTION_ENAPROAMING.equalsIgnoreCase(action);
258 if (!"yes".equalsIgnoreCase(update)) {
259 message = getMessage(enable ? "action.aproaming-enable" : "action.aproaming-disable");
260 actionUrl = buildActionUrl(uid, action);
263 api.setApRoaming(enable);
264 message = getMessage("action.aproaming-confirm", enable ? "enabled" : "disabled");
265 } catch (ShellyApiException e) {
266 message = getMessage("action.aproaming-failed", e.toString());
271 case ACTION_ENRANGEEXT:
272 case ACTION_DISRANGEEXT:
273 enable = ACTION_ENRANGEEXT.equalsIgnoreCase(action);
274 if (!"yes".equalsIgnoreCase(update)) {
275 message = getMessage(
276 enable ? "action.setwifirangeext-enable" : "action.setwifirangeext-disable");
277 actionUrl = buildActionUrl(uid, action);
280 boolean res = api.setWiFiRangeExtender(enable);
282 message = getMessageP("action.restart.info", MCINFO);
283 actionButtonLabel = "Ok";
284 actionUrl = buildActionUrl(uid, ACTION_RESTART);
286 message = getMessage("action.setwifirangeext-confirm", res ? "yes" : "no");
289 } catch (ShellyApiException e) {
290 message = getMessage("action.setwifirangeext-failed", e.toString());
296 case ACTION_ENETHERNET:
297 case ACTION_DISETHERNET:
298 enable = ACTION_ENETHERNET.equalsIgnoreCase(action);
299 if (!"yes".equalsIgnoreCase(update)) {
300 message = getMessage(enable ? "action.setethernet-enable" : "action.setethernet-disable");
301 actionUrl = buildActionUrl(uid, action);
304 boolean res = api.setEthernet(enable);
306 message = getMessageP("action.restart.info", MCINFO);
307 actionButtonLabel = "Ok";
308 actionUrl = buildActionUrl(uid, ACTION_RESTART);
310 message = getMessage("action.setethernet-confirm", res ? "yes" : "no");
313 } catch (ShellyApiException e) {
314 message = getMessage("action.setethernet-failed", e.toString());
319 case ACTION_ENBLUETOOTH:
320 case ACTION_DISBLUETOOTH:
321 enable = ACTION_ENBLUETOOTH.equalsIgnoreCase(action);
322 if (!"yes".equalsIgnoreCase(update)) {
323 message = getMessage(enable ? "action.setbluetooth-enable" : "action.setbluetooth-disable");
324 actionUrl = buildActionUrl(uid, action);
327 boolean res = api.setBluetooth(enable);
329 message = getMessageP("action.restart.info", MCINFO);
330 actionButtonLabel = "Ok";
331 actionUrl = buildActionUrl(uid, ACTION_RESTART);
333 message = getMessage("action.setbluetooth-confirm", res ? "yes" : "no");
336 } catch (ShellyApiException e) {
337 message = getMessage("action.setbluetooth-failed", e.toString());
346 message = api.getDebugLog(ACTION_GETDEB.equalsIgnoreCase(action) ? "log" : "log1");
347 message = message.replaceAll("[\r]", "").replaceAll("[\r\n]", "<br>");
348 } catch (ShellyApiException e) {
349 message = getMessage("action.getdebug-failed", e.toString());
355 logger.warn("{}: {}", LOG_PREFIX, getMessage("action.unknown", action));
358 properties.put(ATTRIBUTE_ACTION, getString(actions.get(action))); // get description for command
359 properties.put(ATTRIBUTE_ACTION_BUTTON, actionButtonLabel);
360 properties.put(ATTRIBUTE_ACTION_URL, actionUrl);
361 message = fillAttributes(message, properties);
362 properties.put(ATTRIBUTE_MESSAGE, message);
363 properties.put(ATTRIBUTE_REFRESH, String.valueOf(refreshTimer));
364 html += loadHTML(ACTION_HTML, properties);
366 th.requestUpdates(1, refreshTimer > 0); // trigger background update
370 html += loadHTML(FOOTER_HTML, properties);
371 return new ShellyMgrResponse(html, HttpStatus.OK_200);
374 public static Map<String, String> getActions(ShellyDeviceProfile profile) {
375 Map<String, String> list = new LinkedHashMap<>();
376 boolean gen2 = profile.isGen2;
378 list.put(ACTION_RES_STATS, "Reset Statistics");
379 if (!profile.isBlu) {
380 list.put(ACTION_RESTART, "Reboot Device");
382 if (!gen2 || !profile.isBlu) {
383 list.put(ACTION_PROTECT, "Protect Device");
386 if (profile.settings.coiot != null && profile.settings.coiot.peer != null) {
387 boolean mcast = profile.settings.coiot.peer.isEmpty()
388 || SHELLY_COIOT_MCAST.equalsIgnoreCase(profile.settings.coiot.peer) || profile.isMotion;
389 list.put(mcast ? ACTION_SETCOIOT_PEER : ACTION_SETCOIOT_MCAST,
390 mcast ? "Set CoIoT Peer Mode" : "Set CoIoT Multicast Mode");
392 if (profile.isSensor && !profile.isMotion && profile.settings.wifiSta != null
393 && getBool(profile.settings.wifiSta.enabled)) {
394 // FW 1.10+: Reset STA list, force WiFi rescan and connect to stringest AP
395 list.put(ACTION_RESSTA, "Reconnect WiFi");
397 if (!gen2 && profile.settings.apRoaming != null && profile.settings.apRoaming.enabled != null) {
398 list.put(!profile.settings.apRoaming.enabled ? ACTION_ENAPROAMING : ACTION_DISAPROAMING,
399 !profile.settings.apRoaming.enabled ? "Enable WiFi Roaming" : "Disable WiFi Roaming");
401 if (!gen2 && profile.settings.wifiRecoveryReboot != null) {
402 list.put(!profile.settings.wifiRecoveryReboot ? ACTION_ENWIFIREC : ACTION_DISWIFIREC,
403 !profile.settings.wifiRecoveryReboot ? "Enable WiFi Recovery" : "Disable WiFi Recovery");
405 if (profile.settings.wifiAp != null && profile.settings.wifiAp.rangeExtender != null) {
406 list.put(!profile.settings.wifiAp.rangeExtender ? ACTION_ENRANGEEXT : ACTION_DISRANGEEXT,
407 !profile.settings.wifiAp.rangeExtender ? "Enable Range Extender" : "Disable Range Extender");
409 if (profile.settings.ethernet != null) {
410 list.put(!profile.settings.ethernet ? ACTION_ENETHERNET : ACTION_DISETHERNET,
411 !profile.settings.ethernet ? "Enable Ethernet" : "Disable Ethernet");
413 if (profile.settings.bluetooth != null) {
414 list.put(!profile.settings.bluetooth ? ACTION_ENBLUETOOTH : ACTION_DISBLUETOOTH,
415 !profile.settings.bluetooth ? "Enable Bluetooth" : "Disable Bluetooth");
418 if (!profile.isBlu) {
419 boolean set = profile.settings.cloud != null && getBool(profile.settings.cloud.enabled);
420 list.put(set ? ACTION_DISCLOUD : ACTION_ENCLOUD, set ? "Disable Cloud" : "Enable Cloud");
422 list.put(ACTION_RESET, "-Factory Reset");
425 if (!gen2 && profile.extFeatures) {
426 list.put(ACTION_OTACHECK, "Check for Update");
427 boolean debug_enable = getBool(profile.settings.debugEnable);
428 list.put(!debug_enable ? ACTION_ENDEBUG : ACTION_DISDEBUG,
429 !debug_enable ? "Enable Debug" : "Disable Debug");
431 list.put(ACTION_GETDEB, "Get Debug log");
432 list.put(ACTION_GETDEB1, "Get Debug log1");
439 private String buildActionUrl(String uid, String action) {
440 return SHELLY_MGR_ACTION_URI + "?" + URLPARM_ACTION + "=" + action + "&" + URLPARM_UID + "=" + urlEncode(uid)
441 + "&" + URLPARM_UPDATE + "=yes";
444 private void setRestarted(ShellyManagerInterface th, String uid) {
445 th.setThingOffline(ThingStatusDetail.GONE, "offline.status-error-restarted");
446 scheduleUpdate(th, uid + "_upgrade", 25); // wait 25s before refresh