]> git.basschouten.com Git - openhab-addons.git/blob
91c1a5bb0dd21424a9b5384240e42d5bfe354122
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 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.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.*;
19
20 import java.util.HashMap;
21 import java.util.LinkedHashMap;
22 import java.util.Map;
23
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;
42
43 /**
44  * {@link ShellyManagerActionPage} implements the Shelly Manager's action page
45  *
46  * @author Markus Michels - Initial contribution
47  */
48 @NonNullByDefault
49 public class ShellyManagerActionPage extends ShellyManagerPage {
50     private final Logger logger = LoggerFactory.getLogger(ShellyManagerActionPage.class);
51
52     public ShellyManagerActionPage(ConfigurationAdmin configurationAdmin, ShellyTranslationProvider translationProvider,
53             HttpClient httpClient, String localIp, int localPort, ShellyHandlerFactory handlerFactory) {
54         super(configurationAdmin, translationProvider, httpClient, localIp, localPort, handlerFactory);
55     }
56
57     @Override
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);
65         }
66
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);
72
73         ShellyManagerInterface th = getThingHandler(uid);
74         if (th != null) {
75             fillProperties(properties, uid, th);
76
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);
81             String message = "";
82
83             ShellyThingConfiguration config = getThingConfig(th, properties);
84             ShellyDeviceProfile profile = th.getProfile();
85             ShellyApiInterface api = th.getApi();
86             new Shelly1HttpApi(uid, config, httpClient);
87
88             int refreshTimer = 0;
89             switch (action) {
90                 case ACTION_RES_STATS:
91                     th.resetStats();
92                     message = getMessageP("action.resstats.confirm", MCINFO);
93                     refreshTimer = 3;
94                     break;
95                 case ACTION_RESTART:
96                     if ("yes".equalsIgnoreCase(update)) {
97                         message = getMessageP("action.restart.info", MCINFO);
98                         actionButtonLabel = "Ok";
99                         new Thread(() -> { // schedule asynchronous reboot
100                             try {
101                                 api.deviceReboot();
102                             } catch (ShellyApiException e) {
103                                 // maybe the device restarts before returning the http response
104                             }
105                             setRestarted(th, uid); // refresh after reboot
106                         }).start();
107                         refreshTimer = profile.isMotion ? 60 : 30;
108                     } else {
109                         message = getMessageS("action.restart.confirm", MCINFO);
110                         actionUrl = buildActionUrl(uid, action);
111                     }
112                     break;
113                 case ACTION_PROTECT:
114                     // Get device settings
115                     if (config.userId.isEmpty() || config.password.isEmpty()) {
116                         message = getMessageP("action.protect.id-missing", MCWARNING);
117                         break;
118                     }
119
120                     if (!"yes".equalsIgnoreCase(update)) {
121                         ShellySettingsLogin status = api.getLoginSettings();
122                         message = getMessage("action.protect.status", getBool(status.enabled) ? "enabled" : "disabled",
123                                 status.username)
124                                 + getMessageP("action.protect.new", MCINFO, config.userId, config.password);
125                         actionUrl = buildActionUrl(uid, action);
126                     } else {
127                         api.setLoginCredentials(config.userId, config.password);
128                         message = getMessageP("action.protect.confirm", MCINFO, config.userId, config.password);
129                         refreshTimer = 3;
130                     }
131                     break;
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);
137                         break;
138                     }
139
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";
144
145                     if (profile.isMotion && action.equalsIgnoreCase(ACTION_SETCOIOT_MCAST)) {
146                         // feature not available
147                         message = getMessageP("coiot.multicast-not-supported", "warning", displayPeer);
148                         break;
149                     }
150
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);
156                     } else {
157                         new Thread(() -> { // schedule asynchronous reboot
158                             try {
159                                 api.setCoIoTPeer(newPeer);
160                                 api.deviceReboot();
161                             } catch (ShellyApiException e) {
162                                 // maybe the device restarts before returning the http response
163                             }
164                             setRestarted(th, uid); // refresh after reboot
165                         }).start();
166
167                         // The device needs a restart after changing the peer mode
168                         message = getMessageP("action.restart.info", MCINFO);
169                         refreshTimer = 30;
170                     }
171                     break;
172                 case ACTION_ENCLOUD:
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");
177                     refreshTimer = 20;
178                     break;
179                 case ACTION_RESET:
180                     if (!"yes".equalsIgnoreCase(update)) {
181                         message = getMessageP("action.reset.warning", MCWARNING, serviceName);
182                         actionUrl = buildActionUrl(uid, action);
183                     } else {
184                         new Thread(() -> { // schedule asynchronous reboot
185                             try {
186                                 api.factoryReset();
187                                 setRestarted(th, uid);
188                             } catch (ShellyApiException e) {
189                                 // maybe the device restarts before returning the http response
190                             }
191                         }).start();
192                         message = getMessageP("action.reset.confirm", MCINFO, serviceName);
193                         refreshTimer = 5;
194                     }
195                     break;
196                 case ACTION_OTACHECK:
197                     try {
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());
203                     }
204                     refreshTimer = 3;
205                     break;
206                 case ACTION_ENDEBUG:
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);
212                     } else {
213                         new Thread(() -> { // schedule asynchronous reboot
214                             try {
215                                 api.setDebug(enable);
216                             } catch (ShellyApiException e) {
217                                 // maybe the device restarts before returning the http response
218                             }
219                         }).start();
220
221                         message = getMessage("action.debug-confirm", enable ? "enabled" : "disabled");
222                         refreshTimer = 3;
223                     }
224                     break;
225                 case ACTION_RESSTA:
226                     if (!"yes".equalsIgnoreCase(update)) {
227                         message = getMessage("action.resetsta-info");
228                         actionUrl = buildActionUrl(uid, action);
229                     } else {
230                         try {
231                             api.resetStaCache();
232                             message = getMessage("action.resetsta-confirm");
233                         } catch (ShellyApiException e) {
234                             message = getMessageP("action.resetsta-failed", e.toString());
235                         }
236                         refreshTimer = 10;
237                     }
238                     break;
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);
245                     } else {
246                         try {
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());
251                         }
252                         refreshTimer = 3;
253                     }
254                     break;
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);
261                     } else {
262                         try {
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());
267                         }
268                         refreshTimer = 3;
269                     }
270                     break;
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);
278                     } else {
279                         try {
280                             boolean res = api.setWiFiRangeExtender(enable);
281                             if (res) {
282                                 message = getMessageP("action.restart.info", MCINFO);
283                                 actionButtonLabel = "Ok";
284                                 actionUrl = buildActionUrl(uid, ACTION_RESTART);
285                             } else {
286                                 message = getMessage("action.setwifirangeext-confirm", res ? "yes" : "no");
287                                 refreshTimer = 5;
288                             }
289                         } catch (ShellyApiException e) {
290                             message = getMessage("action.setwifirangeext-failed", e.toString());
291                             refreshTimer = 10;
292                         }
293                     }
294                     break;
295
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);
302                     } else {
303                         try {
304                             boolean res = api.setEthernet(enable);
305                             if (res) {
306                                 message = getMessageP("action.restart.info", MCINFO);
307                                 actionButtonLabel = "Ok";
308                                 actionUrl = buildActionUrl(uid, ACTION_RESTART);
309                             } else {
310                                 message = getMessage("action.setethernet-confirm", res ? "yes" : "no");
311                                 refreshTimer = 5;
312                             }
313                         } catch (ShellyApiException e) {
314                             message = getMessage("action.setethernet-failed", e.toString());
315                             refreshTimer = 10;
316                         }
317                     }
318                     break;
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);
325                     } else {
326                         try {
327                             boolean res = api.setBluetooth(enable);
328                             if (res) {
329                                 message = getMessageP("action.restart.info", MCINFO);
330                                 actionButtonLabel = "Ok";
331                                 actionUrl = buildActionUrl(uid, ACTION_RESTART);
332                             } else {
333                                 message = getMessage("action.setbluetooth-confirm", res ? "yes" : "no");
334                                 refreshTimer = 5;
335                             }
336                         } catch (ShellyApiException e) {
337                             message = getMessage("action.setbluetooth-failed", e.toString());
338                             refreshTimer = 10;
339                         }
340                     }
341                     break;
342
343                 case ACTION_GETDEB:
344                 case ACTION_GETDEB1:
345                     try {
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());
350                     }
351                     break;
352                 case ACTION_NONE:
353                     break;
354                 default:
355                     logger.warn("{}: {}", LOG_PREFIX, getMessage("action.unknown", action));
356             }
357
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);
365
366             th.requestUpdates(1, refreshTimer > 0); // trigger background update
367         }
368
369         properties.clear();
370         html += loadHTML(FOOTER_HTML, properties);
371         return new ShellyMgrResponse(html, HttpStatus.OK_200);
372     }
373
374     public static Map<String, String> getActions(ShellyDeviceProfile profile) {
375         Map<String, String> list = new LinkedHashMap<>();
376         boolean gen2 = profile.isGen2;
377
378         list.put(ACTION_RES_STATS, "Reset Statistics");
379         list.put(ACTION_RESTART, "Reboot Device");
380         if (gen2) {
381             list.put(ACTION_PROTECT, "Protect Device");
382         }
383
384         if ((profile.settings.coiot != null) && profile.settings.coiot.peer != null) {
385             boolean mcast = profile.settings.coiot.peer.isEmpty()
386                     || SHELLY_COIOT_MCAST.equalsIgnoreCase(profile.settings.coiot.peer) || profile.isMotion;
387             list.put(mcast ? ACTION_SETCOIOT_PEER : ACTION_SETCOIOT_MCAST,
388                     mcast ? "Set CoIoT Peer Mode" : "Set CoIoT Multicast Mode");
389         }
390         if (profile.isSensor && !profile.isMotion && profile.settings.wifiSta != null
391                 && profile.settings.wifiSta.enabled) {
392             // FW 1.10+: Reset STA list, force WiFi rescan and connect to stringest AP
393             list.put(ACTION_RESSTA, "Reconnect WiFi");
394         }
395         if (!gen2 && profile.settings.apRoaming != null) {
396             list.put(!profile.settings.apRoaming.enabled ? ACTION_ENAPROAMING : ACTION_DISAPROAMING,
397                     !profile.settings.apRoaming.enabled ? "Enable WiFi Roaming" : "Disable WiFi Roaming");
398         }
399         if (!gen2 && profile.settings.wifiRecoveryReboot != null) {
400             list.put(!profile.settings.wifiRecoveryReboot ? ACTION_ENWIFIREC : ACTION_DISWIFIREC,
401                     !profile.settings.wifiRecoveryReboot ? "Enable WiFi Recovery" : "Disable WiFi Recovery");
402         }
403         if (profile.settings.wifiAp != null && profile.settings.wifiAp.rangeExtender != null) {
404             list.put(!profile.settings.wifiAp.rangeExtender ? ACTION_ENRANGEEXT : ACTION_DISRANGEEXT,
405                     !profile.settings.wifiAp.rangeExtender ? "Enable Range Extender" : "Disable Range Extender");
406         }
407         if (profile.settings.ethernet != null) {
408             list.put(!profile.settings.ethernet ? ACTION_ENETHERNET : ACTION_DISETHERNET,
409                     !profile.settings.ethernet ? "Enable Ethernet" : "Disable Ethernet");
410         }
411         if (profile.settings.bluetooth != null) {
412             list.put(!profile.settings.bluetooth ? ACTION_ENBLUETOOTH : ACTION_DISBLUETOOTH,
413                     !profile.settings.bluetooth ? "Enable Bluetooth" : "Disable Bluetooth");
414         }
415
416         boolean set = profile.settings.cloud != null && profile.settings.cloud.enabled;
417         list.put(set ? ACTION_DISCLOUD : ACTION_ENCLOUD, set ? "Disable Cloud" : "Enable Cloud");
418
419         list.put(ACTION_RESET, "-Factory Reset");
420         if (!gen2 && profile.extFeatures) {
421             list.put(ACTION_OTACHECK, "Check for Update");
422             boolean debug_enable = getBool(profile.settings.debugEnable);
423             list.put(!debug_enable ? ACTION_ENDEBUG : ACTION_DISDEBUG,
424                     !debug_enable ? "Enable Debug" : "Disable Debug");
425             if (debug_enable) {
426                 list.put(ACTION_GETDEB, "Get Debug log");
427                 list.put(ACTION_GETDEB1, "Get Debug log1");
428             }
429         }
430
431         return list;
432     }
433
434     private String buildActionUrl(String uid, String action) {
435         return SHELLY_MGR_ACTION_URI + "?" + URLPARM_ACTION + "=" + action + "&" + URLPARM_UID + "=" + urlEncode(uid)
436                 + "&" + URLPARM_UPDATE + "=yes";
437     }
438
439     private void setRestarted(ShellyManagerInterface th, String uid) {
440         th.setThingOffline(ThingStatusDetail.GONE, "offline.status-error-restarted");
441         scheduleUpdate(th, uid + "_upgrade", 25); // wait 25s before refresh
442     }
443 }