]> git.basschouten.com Git - openhab-addons.git/blob
662a01c7f4550994f63ff15518e7db3f351f5a85
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2024 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.api2;
14
15 import static org.openhab.binding.shelly.internal.ShellyBindingConstants.*;
16 import static org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.*;
17 import static org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.*;
18 import static org.openhab.binding.shelly.internal.discovery.ShellyThingCreator.THING_TYPE_SHELLYPRO2_RELAY_STR;
19 import static org.openhab.binding.shelly.internal.util.ShellyUtils.*;
20
21 import java.io.BufferedReader;
22 import java.io.IOException;
23 import java.io.InputStream;
24 import java.io.InputStreamReader;
25 import java.io.UncheckedIOException;
26 import java.util.ArrayList;
27 import java.util.List;
28 import java.util.Map;
29 import java.util.stream.Collectors;
30
31 import org.eclipse.jdt.annotation.NonNullByDefault;
32 import org.eclipse.jdt.annotation.Nullable;
33 import org.eclipse.jetty.client.HttpClient;
34 import org.eclipse.jetty.http.HttpStatus;
35 import org.eclipse.jetty.websocket.api.StatusCode;
36 import org.openhab.binding.shelly.internal.api.ShellyApiException;
37 import org.openhab.binding.shelly.internal.api.ShellyApiInterface;
38 import org.openhab.binding.shelly.internal.api.ShellyApiResult;
39 import org.openhab.binding.shelly.internal.api.ShellyDeviceProfile;
40 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyInputState;
41 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyOtaCheckResult;
42 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyRollerStatus;
43 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySensorSleepMode;
44 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsDevice;
45 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsDimmer;
46 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsEMeter;
47 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsLogin;
48 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsMeter;
49 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsRelay;
50 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsStatus;
51 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsUpdate;
52 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsWiFiNetwork;
53 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyShortLightStatus;
54 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyShortStatusRelay;
55 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyStatusLight;
56 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyStatusRelay;
57 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyStatusSensor;
58 import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2APClientList;
59 import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2AuthChallenge;
60 import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2ConfigParms;
61 import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2DeviceConfig.Shelly2DeviceConfigSta;
62 import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2DeviceConfig.Shelly2GetConfigResult;
63 import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2DeviceConfigAp;
64 import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2DeviceConfigAp.Shelly2DeviceConfigApRE;
65 import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2DeviceSettings;
66 import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2DeviceStatus.Shelly2DeviceStatusLight;
67 import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2DeviceStatus.Shelly2DeviceStatusResult;
68 import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2DeviceStatus.Shelly2DeviceStatusSys.Shelly2DeviceStatusSysAvlUpdate;
69 import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2NotifyEvent;
70 import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2RpcBaseMessage;
71 import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2RpcNotifyEvent;
72 import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2RpcNotifyStatus;
73 import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2RpcNotifyStatus.Shelly2NotifyStatus;
74 import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2RpcRequest;
75 import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2RpcRequest.Shelly2RpcRequestParams;
76 import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2WsConfigResponse;
77 import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2WsConfigResponse.Shelly2WsConfigResult;
78 import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.ShellyScriptListResponse;
79 import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.ShellyScriptListResponse.ShellyScriptListEntry;
80 import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.ShellyScriptPutCodeParams;
81 import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.ShellyScriptResponse;
82 import org.openhab.binding.shelly.internal.config.ShellyThingConfiguration;
83 import org.openhab.binding.shelly.internal.handler.ShellyThingInterface;
84 import org.openhab.binding.shelly.internal.handler.ShellyThingTable;
85 import org.openhab.binding.shelly.internal.util.ShellyVersionDTO;
86 import org.openhab.core.library.unit.SIUnits;
87 import org.openhab.core.thing.ThingStatusDetail;
88 import org.slf4j.Logger;
89 import org.slf4j.LoggerFactory;
90
91 /**
92  * {@link Shelly2ApiRpc} implements Gen2 RPC interface
93  *
94  * @author Markus Michels - Initial contribution
95  */
96 @NonNullByDefault
97 public class Shelly2ApiRpc extends Shelly2ApiClient implements ShellyApiInterface, Shelly2RpctInterface {
98     private final Logger logger = LoggerFactory.getLogger(Shelly2ApiRpc.class);
99     private final @Nullable ShellyThingTable thingTable;
100
101     protected boolean initialized = false;
102     private boolean discovery = false;
103     private Shelly2RpcSocket rpcSocket = new Shelly2RpcSocket();
104     private @Nullable Shelly2AuthChallenge authInfo;
105
106     /**
107      * Regular constructor - called by Thing handler
108      *
109      * @param thingName Symbolic thing name
110      * @param thing Thing Handler (ThingHandlerInterface)
111      */
112     public Shelly2ApiRpc(String thingName, ShellyThingTable thingTable, ShellyThingInterface thing) {
113         super(thingName, thing);
114         this.thingName = thingName;
115         this.thing = thing;
116         this.thingTable = thingTable;
117     }
118
119     /**
120      * Simple initialization - called by discovery handler
121      *
122      * @param thingName Symbolic thing name
123      * @param config Thing Configuration
124      * @param httpClient HTTP Client to be passed to ShellyHttpClient
125      */
126     public Shelly2ApiRpc(String thingName, ShellyThingConfiguration config, HttpClient httpClient) {
127         super(thingName, config, httpClient);
128         this.thingName = thingName;
129         this.thingTable = null;
130         this.discovery = true;
131     }
132
133     @Override
134     public void initialize() throws ShellyApiException {
135         if (initialized) {
136             logger.debug("{}: Disconnect Rpc Socket on initialize", thingName);
137             disconnect();
138         }
139         rpcSocket = new Shelly2RpcSocket(thingName, thingTable, config.deviceIp);
140         rpcSocket.addMessageHandler(this);
141         initialized = true;
142     }
143
144     @Override
145     public boolean isInitialized() {
146         return initialized;
147     }
148
149     @Override
150     public void startScan() {
151         try {
152             if (getProfile().isBlu) {
153                 installScript(SHELLY2_BLU_GWSCRIPT, config.enableBluGateway);
154             }
155         } catch (ShellyApiException e) {
156         }
157     }
158
159     @SuppressWarnings("null")
160     @Override
161     public ShellyDeviceProfile getDeviceProfile(String thingType, @Nullable ShellySettingsDevice devInfo)
162             throws ShellyApiException {
163         ShellyDeviceProfile profile = thing != null ? getProfile() : new ShellyDeviceProfile();
164
165         if (devInfo != null) {
166             profile.device = devInfo;
167         }
168         if (profile.device.type == null) {
169             profile.device = getDeviceInfo();
170         }
171
172         Shelly2GetConfigResult dc = apiRequest(SHELLYRPC_METHOD_GETCONFIG, null, Shelly2GetConfigResult.class);
173         profile.isGen2 = true;
174         profile.settingsJson = gson.toJson(dc);
175         profile.thingName = thingName;
176         profile.settings.name = profile.status.name = dc.sys.device.name;
177         profile.name = getString(profile.settings.name);
178         profile.settings.timezone = getString(dc.sys.location.tz);
179         profile.settings.discoverable = getBool(dc.sys.device.discoverable);
180         if (dc.wifi != null && dc.wifi.ap != null && dc.wifi.ap.rangeExtender != null) {
181             profile.settings.wifiAp.rangeExtender = getBool(dc.wifi.ap.rangeExtender.enable);
182         }
183         if (dc.cloud != null) {
184             profile.settings.cloud.enabled = getBool(dc.cloud.enable);
185         }
186         if (dc.mqtt != null) {
187             profile.settings.mqtt.enable = getBool(dc.mqtt.enable);
188         }
189         if (dc.sys.sntp != null) {
190             profile.settings.sntp.server = dc.sys.sntp.server;
191         }
192
193         profile.isRoller = dc.cover0 != null;
194         profile.settings.relays = fillRelaySettings(profile, dc);
195         profile.settings.inputs = fillInputSettings(profile, dc);
196         profile.settings.rollers = fillRollerSettings(profile, dc);
197
198         profile.isEMeter = true;
199         profile.numInputs = profile.settings.inputs != null ? profile.settings.inputs.size() : 0;
200         profile.numRelays = profile.settings.relays != null ? profile.settings.relays.size() : 0;
201         profile.numRollers = profile.settings.rollers != null ? profile.settings.rollers.size() : 0;
202         profile.hasRelays = profile.numRelays > 0 || profile.numRollers > 0;
203         if (getString(profile.device.mode).isEmpty() && profile.hasRelays) {
204             profile.device.mode = profile.isRoller ? SHELLY_CLASS_ROLLER : SHELLY_CLASS_RELAY;
205         }
206
207         ShellySettingsDevice device = profile.device;
208         profile.isGen2 = device.gen == 2;
209         if (config.serviceName.isEmpty()) {
210             config.serviceName = getString(profile.device.hostname);
211         }
212         profile.settings.fw = device.fw;
213         profile.fwDate = substringBefore(substringBefore(device.fw, "/"), "-");
214         profile.fwVersion = profile.status.update.oldVersion = ShellyDeviceProfile.extractFwVersion(device.fw);
215         profile.status.hasUpdate = profile.status.update.hasUpdate = false;
216
217         if (dc.eth != null) {
218             profile.settings.ethernet = getBool(dc.eth.enable);
219         }
220         if (dc.ble != null) {
221             profile.settings.bluetooth = getBool(dc.ble.enable);
222         }
223
224         profile.settings.wifiSta = new ShellySettingsWiFiNetwork();
225         profile.settings.wifiSta1 = new ShellySettingsWiFiNetwork();
226         fillWiFiSta(dc.wifi.sta, profile.settings.wifiSta);
227         fillWiFiSta(dc.wifi.sta1, profile.settings.wifiSta1);
228         if (dc.wifi.ap != null && dc.wifi.ap.rangeExtender != null) {
229             profile.settings.rangeExtender = getBool(dc.wifi.ap.rangeExtender.enable);
230         }
231
232         profile.numMeters = 0;
233         if (profile.hasRelays) {
234             profile.status.relays = new ArrayList<>();
235             relayStatus.relays = new ArrayList<>();
236             profile.numMeters = profile.isRoller ? profile.numRollers : profile.numRelays;
237             for (int i = 0; i < profile.numRelays; i++) {
238                 profile.status.relays.add(new ShellySettingsRelay());
239                 relayStatus.relays.add(new ShellyShortStatusRelay());
240             }
241         }
242
243         if (profile.numInputs > 0) {
244             profile.status.inputs = new ArrayList<>();
245             relayStatus.inputs = new ArrayList<>();
246             for (int i = 0; i < profile.numInputs; i++) {
247                 ShellyInputState input = new ShellyInputState();
248                 input.input = 0;
249                 input.event = "";
250                 input.eventCount = 0;
251                 profile.status.inputs.add(input);
252                 relayStatus.inputs.add(input);
253             }
254         }
255
256         // handle special cases, because there is no indicator for a meter in GetConfig
257         // Pro 3EM has 3 meters
258         // Pro 2 has 2 relays, but no meters
259         // Mini PM has 1 meter, but no relay
260         if (thingType.equals(THING_TYPE_SHELLYPRO2_RELAY_STR)) {
261             profile.numMeters = 0;
262         } else if (dc.pm10 != null) {
263             profile.numMeters = 1;
264         } else if (dc.em0 != null) {
265             profile.numMeters = 3;
266         } else if (dc.em10 != null) {
267             profile.numMeters = 2;
268         }
269
270         if (profile.numMeters > 0) {
271             profile.status.meters = new ArrayList<>();
272             profile.status.emeters = new ArrayList<>();
273             relayStatus.meters = new ArrayList<>();
274
275             for (int i = 0; i < profile.numMeters; i++) {
276                 profile.status.meters.add(new ShellySettingsMeter());
277                 profile.status.emeters.add(new ShellySettingsEMeter());
278                 relayStatus.meters.add(new ShellySettingsMeter());
279             }
280         }
281
282         if (profile.isRoller) {
283             profile.status.rollers = new ArrayList<>();
284             for (int i = 0; i < profile.numRollers; i++) {
285                 ShellyRollerStatus rs = new ShellyRollerStatus();
286                 profile.status.rollers.add(rs);
287                 rollerStatus.add(rs);
288             }
289         }
290
291         if (profile.isDimmer) {
292             profile.settings.dimmers = new ArrayList<>();
293             profile.settings.dimmers.add(new ShellySettingsDimmer());
294             profile.status.dimmers = new ArrayList<>();
295             profile.status.dimmers.add(new ShellyShortLightStatus());
296             fillDimmerSettings(profile, dc);
297         }
298         profile.status.lights = profile.isBulb ? new ArrayList<>() : null;
299         profile.status.thermostats = profile.isTRV ? new ArrayList<>() : null;
300
301         if (profile.hasBattery) {
302             profile.settings.sleepMode = new ShellySensorSleepMode();
303             profile.settings.sleepMode.unit = "m";
304             profile.settings.sleepMode.period = dc.sys.sleep != null ? dc.sys.sleep.wakeupPeriod / 60 : 720;
305             checkSetWsCallback();
306         }
307
308         if (dc.led != null) {
309             profile.settings.ledStatusDisable = !getBool(dc.led.sysLedEnable);
310             profile.settings.ledPowerDisable = "off".equals(getString(dc.led.powerLed));
311         }
312
313         profile.initialized = true;
314         if (!discovery) {
315             getStatus(); // make sure profile.status is initialized (e.g,. relay/meter status)
316             asyncApiRequest(SHELLYRPC_METHOD_GETSTATUS); // request periodic status updates from device
317
318             try {
319                 if (profile.alwaysOn && config.enableBluGateway != null) {
320                     logger.debug("{}: BLU Gateway support is {} for this device", thingName,
321                             config.enableBluGateway ? "enabled" : "disabled");
322                     if (config.enableBluGateway) {
323                         boolean bluetooth = getBool(profile.settings.bluetooth);
324                         if (config.enableBluGateway && !bluetooth) {
325                             logger.info("{}: Bluetooth needs to be enabled to activate BLU Gateway mode", thingName);
326                         }
327                         installScript(SHELLY2_BLU_GWSCRIPT, config.enableBluGateway && bluetooth);
328                     }
329                 }
330             } catch (ShellyApiException e) {
331                 logger.debug("{}: Device config failed", thingName, e);
332             }
333         }
334
335         return profile;
336     }
337
338     private void fillWiFiSta(@Nullable Shelly2DeviceConfigSta from, ShellySettingsWiFiNetwork to) {
339         to.enabled = from != null && !getString(from.ssid).isEmpty();
340         if (from != null) {
341             to.ssid = from.ssid;
342             to.ip = from.ip;
343             to.mask = from.netmask;
344             to.dns = from.nameserver;
345         }
346     }
347
348     private void checkSetWsCallback() throws ShellyApiException {
349         Shelly2ConfigParms wsConfig = apiRequest(SHELLYRPC_METHOD_WSGETCONFIG, null, Shelly2ConfigParms.class);
350         String url = "ws://" + config.localIp + ":" + config.localPort + "/shelly/wsevent";
351         if (!config.localIp.isEmpty() && !getBool(wsConfig.enable)
352                 || !url.equalsIgnoreCase(getString(wsConfig.server))) {
353             logger.debug("{}: A battery device was detected without correct callback, fix it", thingName);
354             wsConfig.enable = true;
355             wsConfig.server = url;
356             Shelly2RpcRequest request = new Shelly2RpcRequest();
357             request.id = 0;
358             request.method = SHELLYRPC_METHOD_WSSETCONFIG;
359             request.params.config = wsConfig;
360             Shelly2WsConfigResponse response = apiRequest(SHELLYRPC_METHOD_WSSETCONFIG, request.params,
361                     Shelly2WsConfigResponse.class);
362             if (response.result != null && response.result.restartRequired) {
363                 logger.info("{}: WebSocket callback was updated, device is restarting", thingName);
364                 getThing().getApi().deviceReboot();
365                 getThing().reinitializeThing();
366             }
367         }
368     }
369
370     protected void installScript(String script, boolean install) throws ShellyApiException {
371         try {
372             ShellyScriptListResponse scriptList = apiRequest(
373                     new Shelly2RpcRequest().withMethod(SHELLYRPC_METHOD_SCRIPT_LIST), ShellyScriptListResponse.class);
374             Integer ourId = -1;
375             String code = "";
376
377             if (install) {
378                 logger.debug("{}: Install or restart script {} on Shelly Device", thingName, script);
379             }
380             boolean running = false, upload = false;
381             for (ShellyScriptListEntry s : scriptList.scripts) {
382                 if (s.name.startsWith(script)) {
383                     ourId = s.id;
384                     running = s.running;
385                     logger.debug("{}: Script {} is already installed, id={}", thingName, script, ourId);
386                     break;
387                 }
388             }
389
390             if (!install) {
391                 if (ourId != -1) {
392                     startScript(ourId, false);
393                     enableScript(script, false);
394                     deleteScript(ourId);
395                     logger.debug("{}: Script {} was disabledd, id={}", thingName, script, ourId);
396                 }
397                 return;
398             }
399
400             // get script code from bundle resources
401             String file = BUNDLE_RESOURCE_SCRIPTS + "/" + script;
402             ClassLoader cl = Shelly2ApiRpc.class.getClassLoader();
403             if (cl != null) {
404                 try (InputStream inputStream = cl.getResourceAsStream(file)) {
405                     if (inputStream != null) {
406                         code = new BufferedReader(new InputStreamReader(inputStream)).lines()
407                                 .collect(Collectors.joining("\n"));
408                     }
409                 } catch (IOException | UncheckedIOException e) {
410                     logger.debug("{}: Installation of script {} failed: Unable to read {} from bundle resources!",
411                             thingName, script, file, e);
412                 }
413             }
414
415             boolean restart = false;
416             if (ourId == -1) {
417                 // script not installed -> install it
418                 upload = true;
419             } else {
420                 try {
421                     // verify that the same code version is active (avoid unnesesary flash updates)
422                     ShellyScriptResponse rsp = apiRequest(
423                             new Shelly2RpcRequest().withMethod(SHELLYRPC_METHOD_SCRIPT_GETCODE).withId(ourId),
424                             ShellyScriptResponse.class);
425                     if (!rsp.data.trim().equals(code.trim())) {
426                         logger.debug("{}: A script version was found, update to newest one", thingName);
427                         upload = true;
428                     } else {
429                         logger.debug("{}: Same script version was found, restart", thingName);
430                         restart = true;
431                     }
432                 } catch (ShellyApiException e) {
433                     logger.debug("{}: Unable to read current script code -> force update (deviced returned: {})",
434                             thingName, e.getMessage());
435                     upload = true;
436                 }
437             }
438
439             if (restart || (running && upload)) {
440                 // first stop running script
441                 startScript(ourId, false);
442                 running = false;
443             }
444             if (upload && ourId != -1) {
445                 // Delete existing script
446                 deleteScript(ourId);
447             }
448
449             if (upload) {
450                 logger.debug("{}: Script will be installed...", thingName);
451
452                 // Create new script, get id
453                 ShellyScriptResponse rsp = apiRequest(
454                         new Shelly2RpcRequest().withMethod(SHELLYRPC_METHOD_SCRIPT_CREATE).withName(script),
455                         ShellyScriptResponse.class);
456                 ourId = rsp.id;
457                 logger.debug("{}: Script has been created, id={}", thingName, ourId);
458                 upload = true;
459             }
460
461             if (upload) {
462                 // Put script code for generated id
463                 ShellyScriptPutCodeParams parms = new ShellyScriptPutCodeParams();
464                 parms.id = ourId;
465                 parms.append = false;
466                 int length = code.length(), processed = 0, chunk = 1;
467                 do {
468                     int nextlen = Math.min(1024, length - processed);
469                     parms.code = code.substring(processed, processed + nextlen);
470                     logger.debug("{}: Uploading chunk {} of script (total {} chars, {} processed)", thingName, chunk,
471                             length, processed);
472                     apiRequest(SHELLYRPC_METHOD_SCRIPT_PUTCODE, parms, String.class);
473                     processed += nextlen;
474                     chunk++;
475                     parms.append = true;
476                 } while (processed < length);
477                 running = false;
478             }
479             if (enableScript(script, true) && upload) {
480                 logger.info("{}: Script {} was {} installed successful", thingName, thingName, script);
481             }
482
483             if (!running) {
484                 running = startScript(ourId, true);
485                 logger.debug("{}: Script {} {}", thingName, script,
486                         running ? "was successfully started" : "failed to start");
487             }
488         } catch (ShellyApiException e) {
489             ShellyApiResult res = e.getApiResult();
490             if (res.httpCode == HttpStatus.NOT_FOUND_404) { // Shely 4Pro
491                 logger.debug("{}: Script {} was not installed, device doesn't support scripts", thingName, script);
492             } else {
493                 logger.debug("{}: Unable to install script {}: {}", thingName, script, res.toString());
494             }
495         }
496     }
497
498     private boolean startScript(int ourId, boolean start) {
499         if (ourId != -1) {
500             try {
501                 apiRequest(new Shelly2RpcRequest()
502                         .withMethod(start ? SHELLYRPC_METHOD_SCRIPT_START : SHELLYRPC_METHOD_SCRIPT_STOP)
503                         .withId(ourId));
504                 return true;
505             } catch (ShellyApiException e) {
506             }
507         }
508         return false;
509     }
510
511     private boolean enableScript(String script, boolean enable) {
512         try {
513             Shelly2RpcRequestParams params = new Shelly2RpcRequestParams().withConfig();
514             params.config.name = script;
515             params.config.enable = enable;
516             apiRequest(SHELLYRPC_METHOD_SCRIPT_SETCONFIG, params, String.class);
517             return true;
518         } catch (ShellyApiException e) {
519             logger.debug("{}: Unable to enable script {}", thingName, script, e);
520             return false;
521         }
522     }
523
524     private boolean deleteScript(int id) {
525         if (id == -1) {
526             throw new IllegalArgumentException("Invalid Script Id");
527         }
528         try {
529             logger.debug("{}: Delete existing script with id{}", thingName, id);
530             apiRequest(new Shelly2RpcRequest().withMethod(SHELLYRPC_METHOD_SCRIPT_DELETE).withId(id));
531             return true;
532         } catch (ShellyApiException e) {
533             logger.debug("{}: Unable to delete script with id {}", thingName, id);
534         }
535         return false;
536     }
537
538     @Override
539     public void onConnect(String deviceIp, boolean connected) {
540         if (thing == null && thingTable != null) {
541             thing = thingTable.getThing(deviceIp);
542             logger.debug("{}: Get thing from thingTable", thingName);
543         }
544     }
545
546     @Override
547     public void onNotifyStatus(Shelly2RpcNotifyStatus message) {
548         logger.debug("{}: NotifyStatus update received: {}", thingName, gson.toJson(message));
549         try {
550             ShellyThingInterface t = thing;
551             if (t == null) {
552                 logger.debug("{}: No matching thing on NotifyStatus for {}, ignore (src={}, dst={}, discovery={})",
553                         thingName, thingName, message.src, message.dst, discovery);
554                 return;
555             }
556             if (t.isStopping()) {
557                 logger.debug("{}: Thing is shutting down, ignore WebSocket message", thingName);
558                 return;
559             }
560             if (!t.isThingOnline() && t.getThingStatusDetail() != ThingStatusDetail.CONFIGURATION_PENDING) {
561                 logger.debug("{}: Thing is not in online state/connectable, ignore NotifyStatus", thingName);
562                 return;
563             }
564
565             getThing().incProtMessages();
566             if (message.error != null) {
567                 if (message.error.code == HttpStatus.UNAUTHORIZED_401 && !getString(message.error.message).isEmpty()) {
568                     // Save nonce for notification
569                     Shelly2AuthChallenge auth = gson.fromJson(message.error.message, Shelly2AuthChallenge.class);
570                     if (auth != null && auth.realm == null) {
571                         logger.debug("{}: Authentication data received: {}", thingName, message.error.message);
572                         authInfo = auth;
573                     }
574                 } else {
575                     logger.debug("{}: Error status received - {} {}", thingName, message.error.code,
576                             message.error.message);
577                     incProtErrors();
578                 }
579             }
580
581             Shelly2NotifyStatus params = message.params;
582             if (params != null) {
583                 if (getThing().getThingStatusDetail() != ThingStatusDetail.FIRMWARE_UPDATING) {
584                     getThing().setThingOnline();
585                 }
586
587                 boolean updated = false;
588                 ShellyDeviceProfile profile = getProfile();
589                 ShellySettingsStatus status = profile.status;
590                 if (params.sys != null) {
591                     if (getBool(params.sys.restartRequired)) {
592                         logger.warn("{}: Device requires restart to activate changes", thingName);
593                     }
594                     status.uptime = params.sys.uptime;
595                 }
596                 status.temperature = SHELLY_API_INVTEMP; // mark invalid
597                 updated |= fillDeviceStatus(status, message.params, true);
598                 if (getDouble(status.temperature) == SHELLY_API_INVTEMP) {
599                     // no device temp available
600                     status.temperature = null;
601                 } else {
602                     if (status.tmp != null) {
603                         updated |= updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_ITEMP,
604                                 toQuantityType(getDouble(status.tmp.tC), DIGITS_NONE, SIUnits.CELSIUS));
605                     }
606                 }
607
608                 profile.status = status;
609                 if (updated) {
610                     getThing().restartWatchdog();
611                 }
612             }
613         } catch (ShellyApiException e) {
614             logger.debug("{}: Unable to process status update", thingName, e);
615             incProtErrors();
616         }
617     }
618
619     @Override
620     public void onNotifyEvent(Shelly2RpcNotifyEvent message) {
621         try {
622             logger.debug("{}: NotifyEvent  received: {}", thingName, gson.toJson(message));
623             ShellyDeviceProfile profile = getProfile();
624
625             getThing().incProtMessages();
626             getThing().restartWatchdog();
627
628             for (Shelly2NotifyEvent e : message.params.events) {
629                 switch (e.event) {
630                     case SHELLY2_EVENT_BTNUP:
631                     case SHELLY2_EVENT_BTNDOWN:
632                         String bgroup = getProfile().getInputGroup(e.id);
633                         updateChannel(bgroup, CHANNEL_INPUT + profile.getInputSuffix(e.id),
634                                 getOnOff(SHELLY2_EVENT_BTNDOWN.equals(getString(e.event))));
635                         getThing().triggerButton(profile.getInputGroup(e.id), e.id,
636                                 mapValue(MAP_INPUT_EVENT_ID, e.event));
637                         break;
638
639                     case SHELLY2_EVENT_1PUSH:
640                     case SHELLY2_EVENT_2PUSH:
641                     case SHELLY2_EVENT_3PUSH:
642                     case SHELLY2_EVENT_LPUSH:
643                     case SHELLY2_EVENT_SLPUSH:
644                     case SHELLY2_EVENT_LSPUSH:
645                         if (e.id < profile.numInputs) {
646                             ShellyInputState input = relayStatus.inputs.get(e.id);
647                             input.event = getString(MAP_INPUT_EVENT_TYPE.get(e.event));
648                             input.eventCount = getInteger(input.eventCount) + 1;
649                             relayStatus.inputs.set(e.id, input);
650                             profile.status.inputs.set(e.id, input);
651
652                             String group = getProfile().getInputGroup(e.id);
653                             updateChannel(group, CHANNEL_STATUS_EVENTTYPE + profile.getInputSuffix(e.id),
654                                     getStringType(input.event));
655                             updateChannel(group, CHANNEL_STATUS_EVENTCOUNT + profile.getInputSuffix(e.id),
656                                     getDecimal(input.eventCount));
657                             getThing().triggerButton(profile.getInputGroup(e.id), e.id,
658                                     mapValue(MAP_INPUT_EVENT_ID, e.event));
659                         }
660                         break;
661                     case SHELLY2_EVENT_CFGCHANGED:
662                         logger.debug("{}: Configuration update detected, re-initialize", thingName);
663                         getThing().requestUpdates(1, true); // refresh config
664                         break;
665
666                     case SHELLY2_EVENT_OTASTART:
667                         logger.debug("{}: Firmware update started: {}", thingName, getString(e.msg));
668                         getThing().postEvent(e.event, true);
669                         getThing().setThingOffline(ThingStatusDetail.FIRMWARE_UPDATING,
670                                 "offline.status-error-fwupgrade");
671                         break;
672                     case SHELLY2_EVENT_OTAPROGRESS:
673                         logger.debug("{}: Firmware update in progress: {}", thingName, getString(e.msg));
674                         getThing().postEvent(e.event, false);
675                         break;
676                     case SHELLY2_EVENT_OTADONE:
677                         logger.debug("{}: Firmware update completed: {}", thingName, getString(e.msg));
678                         getThing().setThingOffline(ThingStatusDetail.CONFIGURATION_PENDING,
679                                 "offline.status-error-restarted");
680                         getThing().requestUpdates(1, true); // refresh config
681                         break;
682                     case SHELLY2_EVENT_SLEEP:
683                         logger.debug("{}: Connection terminated, e.g. device in sleep mode", thingName);
684                         break;
685                     case SHELLY2_EVENT_WIFICONNFAILED:
686                         logger.debug("{}: WiFi connect failed, check setup, reason {}", thingName,
687                                 getInteger(e.reason));
688                         getThing().postEvent(e.event, false);
689                         break;
690                     case SHELLY2_EVENT_WIFIDISCONNECTED:
691                         logger.debug("{}: WiFi disconnected, reason {}", thingName, getInteger(e.reason));
692                         getThing().postEvent(e.event, false);
693                         break;
694                     default:
695                         logger.debug("{}: Event {} was not handled", thingName, e.event);
696                 }
697             }
698         } catch (ShellyApiException e) {
699             logger.debug("{}: Unable to process event", thingName, e);
700             incProtErrors();
701         }
702     }
703
704     @Override
705     public void onMessage(String message) {
706         logger.debug("{}: Unexpected RPC message received: {}", thingName, message);
707         incProtErrors();
708     }
709
710     @Override
711     public void onClose(int statusCode, String description) {
712         try {
713             String reason = getString(description);
714             logger.debug("{}: WebSocket connection closed, status = {}/{}", thingName, statusCode, reason);
715             if ("Bye".equalsIgnoreCase(reason)) {
716                 logger.debug("{}: Device went to sleep mode", thingName);
717             } else if (statusCode == StatusCode.ABNORMAL && !discovery && getProfile().alwaysOn) {
718                 // e.g. device rebooted
719                 thingOffline("WebSocket connection closed abnormal");
720             }
721         } catch (ShellyApiException e) {
722             logger.debug("{}: Exception on onClose()", thingName, e);
723             incProtErrors();
724         }
725     }
726
727     @Override
728     public void onError(Throwable cause) {
729         logger.debug("{}: WebSocket error: {}", thingName, cause.getMessage());
730         if (thing != null && thing.getProfile().alwaysOn) {
731             thingOffline("WebSocket error");
732         }
733     }
734
735     private void thingOffline(String reason) {
736         if (thing != null) { // do not reinit of battery powered devices with sleep mode
737             thing.setThingOffline(ThingStatusDetail.COMMUNICATION_ERROR, "offline.status-error-unexpected-error",
738                     reason);
739         }
740     }
741
742     @Override
743     public ShellySettingsDevice getDeviceInfo() throws ShellyApiException {
744         Shelly2DeviceSettings device = callApi("/shelly", Shelly2DeviceSettings.class);
745         ShellySettingsDevice info = new ShellySettingsDevice();
746         info.hostname = getString(device.id);
747         info.name = getString(device.name);
748         info.fw = getString(device.fw);
749         info.type = getString(device.model);
750         info.mac = getString(device.mac);
751         info.auth = getBool(device.auth);
752         info.gen = getInteger(device.gen);
753         info.mode = mapValue(MAP_PROFILE, device.profile);
754         return info;
755     }
756
757     @Override
758     public ShellySettingsStatus getStatus() throws ShellyApiException {
759         ShellyDeviceProfile profile = getProfile();
760         ShellySettingsStatus status = profile.status;
761         Shelly2DeviceStatusResult ds = apiRequest(SHELLYRPC_METHOD_GETSTATUS, null, Shelly2DeviceStatusResult.class);
762         status.time = ds.sys.time;
763         status.uptime = ds.sys.uptime;
764         status.cloud.connected = getBool(ds.cloud.connected);
765         status.mqtt.connected = getBool(ds.mqtt.connected);
766         status.wifiSta.ssid = getString(ds.wifi.ssid);
767         status.wifiSta.enabled = !status.wifiSta.ssid.isEmpty();
768         status.wifiSta.ip = getString(ds.wifi.staIP);
769         status.wifiSta.rssi = getInteger(ds.wifi.rssi);
770         status.fsFree = ds.sys.fsFree;
771         status.fsSize = ds.sys.fsSize;
772         status.discoverable = getBool(profile.settings.discoverable);
773
774         if (ds.sys.wakeupPeriod != null) {
775             profile.settings.sleepMode.period = ds.sys.wakeupPeriod / 60;
776         }
777
778         if (ds.sys.availableUpdates != null) {
779             status.update.hasUpdate = ds.sys.availableUpdates.stable != null;
780             if (ds.sys.availableUpdates.stable != null) {
781                 status.update.newVersion = ShellyDeviceProfile
782                         .extractFwVersion(getString(ds.sys.availableUpdates.stable.version));
783                 status.hasUpdate = new ShellyVersionDTO().compare(profile.fwVersion, status.update.newVersion) < 0;
784             }
785             if (ds.sys.availableUpdates.beta != null) {
786                 status.update.betaVersion = ShellyDeviceProfile
787                         .extractFwVersion(getString(ds.sys.availableUpdates.beta.version));
788                 status.hasUpdate = new ShellyVersionDTO().compare(profile.fwVersion, status.update.betaVersion) < 0;
789             }
790         }
791
792         if (ds.sys.wakeUpReason != null && ds.sys.wakeUpReason.boot != null)
793
794         {
795             List<Object> values = new ArrayList<>();
796             String boot = getString(ds.sys.wakeUpReason.boot);
797             String cause = getString(ds.sys.wakeUpReason.cause);
798
799             // Index 0 is aggregated status, 1 boot, 2 cause
800             String reason = boot.equals(SHELLY2_WAKEUPO_BOOT_RESTART) ? ALARM_TYPE_RESTARTED : cause;
801             values.add(reason);
802             values.add(ds.sys.wakeUpReason.boot);
803             values.add(ds.sys.wakeUpReason.cause);
804             getThing().updateWakeupReason(values);
805         }
806
807         fillDeviceStatus(status, ds, false);
808         if (getBool(profile.settings.rangeExtender)) {
809             try {
810                 // Get List of AP clients
811                 profile.status.rangeExtender = apiRequest(SHELLYRPC_METHOD_WIFILISTAPCLIENTS, null,
812                         Shelly2APClientList.class);
813                 logger.debug("{}: Range extender is enabled, {} clients connected", thingName,
814                         profile.status.rangeExtender.apClients.size());
815             } catch (ShellyApiException e) {
816                 logger.debug("{}: Range extender is enabled, but unable to read AP client list", thingName, e);
817                 profile.settings.rangeExtender = false;
818             }
819         }
820
821         return status;
822     }
823
824     @Override
825     public void setSleepTime(int value) throws ShellyApiException {
826     }
827
828     @Override
829     public ShellyStatusRelay getRelayStatus(int relayIndex) throws ShellyApiException {
830         if (getProfile().status.wifiSta.ssid == null) {
831             // Update status when not yet initialized
832             getStatus();
833         }
834         return relayStatus;
835     }
836
837     @Override
838     public void setRelayTurn(int id, String turnMode) throws ShellyApiException {
839         ShellyDeviceProfile profile = getProfile();
840         int rIdx = id;
841         if (profile.settings.relays != null) {
842             Integer rid = profile.settings.relays.get(id).id;
843             if (rid != null) {
844                 rIdx = rid;
845             }
846         }
847         Shelly2RpcRequestParams params = new Shelly2RpcRequestParams();
848         params.id = rIdx;
849         params.on = SHELLY_API_ON.equals(turnMode);
850         apiRequest(SHELLYRPC_METHOD_SWITCH_SET, params, String.class);
851     }
852
853     @Override
854     public ShellyRollerStatus getRollerStatus(int rollerIndex) throws ShellyApiException {
855         if (rollerIndex < rollerStatus.size()) {
856             return rollerStatus.get(rollerIndex);
857         }
858         throw new IllegalArgumentException("Invalid rollerIndex on getRollerStatus");
859     }
860
861     @Override
862     public void setRollerTurn(int relayIndex, String turnMode) throws ShellyApiException {
863         String operation = "";
864         switch (turnMode) {
865             case SHELLY_ALWD_ROLLER_TURN_OPEN:
866                 operation = SHELLY2_COVER_CMD_OPEN;
867                 break;
868             case SHELLY_ALWD_ROLLER_TURN_CLOSE:
869                 operation = SHELLY2_COVER_CMD_CLOSE;
870                 break;
871             case SHELLY_ALWD_ROLLER_TURN_STOP:
872                 operation = SHELLY2_COVER_CMD_STOP;
873                 break;
874         }
875
876         apiRequest(new Shelly2RpcRequest().withMethod("Cover." + operation).withId(relayIndex));
877     }
878
879     @Override
880     public void setRollerPos(int relayIndex, int position) throws ShellyApiException {
881         apiRequest(
882                 new Shelly2RpcRequest().withMethod(SHELLYRPC_METHOD_COVER_SETPOS).withId(relayIndex).withPos(position));
883     }
884
885     @Override
886     public ShellyStatusLight getLightStatus() throws ShellyApiException {
887         throw new ShellyApiException("API call not implemented");
888     }
889
890     @Override
891     public ShellyShortLightStatus getLightStatus(int index) throws ShellyApiException {
892         ShellyShortLightStatus status = new ShellyShortLightStatus();
893         Shelly2DeviceStatusLight ls = apiRequest(
894                 new Shelly2RpcRequest().withMethod(SHELLYRPC_METHOD_LIGHT_STATUS).withId(index),
895                 Shelly2DeviceStatusLight.class);
896         status.ison = ls.output;
897         status.hasTimer = ls.timerStartedAt != null;
898         status.timerDuration = getDuration(ls.timerStartedAt, ls.timerDuration);
899         if (ls.brightness != null) {
900             status.brightness = ls.brightness.intValue();
901         }
902         return status;
903     }
904
905     @Override
906     public void setBrightness(int id, int brightness, boolean autoOn) throws ShellyApiException {
907         Shelly2RpcRequestParams params = new Shelly2RpcRequestParams();
908         params.id = id;
909         params.brightness = brightness;
910         params.on = brightness > 0;
911         apiRequest(SHELLYRPC_METHOD_LIGHT_SET, params, String.class);
912     }
913
914     @Override
915     public ShellyShortLightStatus setLightTurn(int id, String turnMode) throws ShellyApiException {
916         Shelly2RpcRequestParams params = new Shelly2RpcRequestParams();
917         params.id = id;
918         params.on = turnMode.equals(SHELLY_API_ON);
919         apiRequest(SHELLYRPC_METHOD_LIGHT_SET, params, String.class);
920         return getLightStatus(id);
921     }
922
923     @Override
924     public ShellyStatusSensor getSensorStatus() throws ShellyApiException {
925         return sensorData;
926     }
927
928     @Override
929     public void setAutoTimer(int index, String timerName, double value) throws ShellyApiException {
930         ShellyDeviceProfile profile = getProfile();
931         boolean isLight = profile.isLight || profile.isDimmer;
932         String method = isLight ? SHELLYRPC_METHOD_LIGHT_SETCONFIG : SHELLYRPC_METHOD_SWITCH_SETCONFIG;
933         String component = isLight ? "Light" : "Switch";
934         Shelly2RpcRequest req = new Shelly2RpcRequest().withMethod(method).withId(index);
935         req.params.withConfig();
936         req.params.config.name = component + index;
937         if (timerName.equals(SHELLY_TIMER_AUTOON)) {
938             req.params.config.autoOn = value > 0;
939             req.params.config.autoOnDelay = value;
940         } else {
941             req.params.config.autoOff = value > 0;
942             req.params.config.autoOffDelay = value;
943         }
944         apiRequest(req);
945     }
946
947     @Override
948     public void setLedStatus(String ledName, boolean value) throws ShellyApiException {
949         Shelly2RpcRequestParams params = new Shelly2RpcRequestParams().withConfig();
950         params.id = 0;
951         if (ledName.equals(SHELLY_LED_STATUS_DISABLE)) {
952             params.config.sysLedEnable = value;
953         } else if (ledName.equals(SHELLY_LED_POWER_DISABLE)) {
954             params.config.powerLed = value ? SHELLY2_POWERLED_OFF : SHELLY2_POWERLED_MATCH;
955         } else {
956             throw new ShellyApiException("API call not implemented for this LED type");
957         }
958         apiRequest(SHELLYRPC_METHOD_LED_SETCONFIG, params, Shelly2WsConfigResult.class);
959     }
960
961     @Override
962     public void resetMeterTotal(int id) throws ShellyApiException {
963         apiRequest(new Shelly2RpcRequest()
964                 .withMethod(getProfile().is3EM ? SHELLYRPC_METHOD_EMDATARESET : SHELLYRPC_METHOD_EM1DATARESET)
965                 .withId(id));
966     }
967
968     @Override
969     public void muteSmokeAlarm(int index) throws ShellyApiException {
970         apiRequest(new Shelly2RpcRequest().withMethod(SHELLYRPC_METHOD_SMOKE_MUTE).withId(index));
971     }
972
973     @Override
974     public ShellySettingsLogin getLoginSettings() throws ShellyApiException {
975         return new ShellySettingsLogin();
976     }
977
978     @Override
979     public ShellySettingsLogin setLoginCredentials(String user, String password) throws ShellyApiException {
980         Shelly2RpcRequestParams params = new Shelly2RpcRequestParams();
981         params.user = "admin";
982         params.realm = config.serviceName;
983         params.ha1 = sha256(params.user + ":" + params.realm + ":" + password);
984         apiRequest(SHELLYRPC_METHOD_AUTHSET, params, String.class);
985
986         ShellySettingsLogin res = new ShellySettingsLogin();
987         res.enabled = true;
988         res.username = params.user;
989         res.password = password;
990         return new ShellySettingsLogin();
991     }
992
993     @Override
994     public boolean setWiFiRangeExtender(boolean enable) throws ShellyApiException {
995         Shelly2RpcRequestParams params = new Shelly2RpcRequestParams().withConfig();
996         params.config.ap = new Shelly2DeviceConfigAp();
997         params.config.ap.rangeExtender = new Shelly2DeviceConfigApRE();
998         params.config.ap.rangeExtender.enable = enable;
999         Shelly2WsConfigResult res = apiRequest(SHELLYRPC_METHOD_WIFISETCONG, params, Shelly2WsConfigResult.class);
1000         return res.restartRequired;
1001     }
1002
1003     @Override
1004     public boolean setEthernet(boolean enable) throws ShellyApiException {
1005         Shelly2RpcRequestParams params = new Shelly2RpcRequestParams().withConfig();
1006         params.config.enable = enable;
1007         Shelly2WsConfigResult res = apiRequest(SHELLYRPC_METHOD_ETHSETCONG, params, Shelly2WsConfigResult.class);
1008         return res.restartRequired;
1009     }
1010
1011     @Override
1012     public boolean setBluetooth(boolean enable) throws ShellyApiException {
1013         Shelly2RpcRequestParams params = new Shelly2RpcRequestParams().withConfig();
1014         params.config.enable = enable;
1015         Shelly2WsConfigResult res = apiRequest(SHELLYRPC_METHOD_BLESETCONG, params, Shelly2WsConfigResult.class);
1016         return res.restartRequired;
1017     }
1018
1019     @Override
1020     public String deviceReboot() throws ShellyApiException {
1021         return apiRequest(SHELLYRPC_METHOD_REBOOT, null, String.class);
1022     }
1023
1024     @Override
1025     public String factoryReset() throws ShellyApiException {
1026         return apiRequest(SHELLYRPC_METHOD_RESET, null, String.class);
1027     }
1028
1029     @Override
1030     public ShellyOtaCheckResult checkForUpdate() throws ShellyApiException {
1031         Shelly2DeviceStatusSysAvlUpdate status = apiRequest(SHELLYRPC_METHOD_CHECKUPD, null,
1032                 Shelly2DeviceStatusSysAvlUpdate.class);
1033         ShellyOtaCheckResult result = new ShellyOtaCheckResult();
1034         result.status = status.stable != null || status.beta != null ? "new" : "ok";
1035         return result;
1036     }
1037
1038     @Override
1039     public ShellySettingsUpdate firmwareUpdate(String fwurl) throws ShellyApiException {
1040         ShellySettingsUpdate res = new ShellySettingsUpdate();
1041         boolean prod = fwurl.contains("update");
1042         boolean beta = fwurl.contains("beta");
1043
1044         Shelly2RpcRequestParams params = new Shelly2RpcRequestParams();
1045         if (prod || beta) {
1046             params.stage = prod ? "stable" : "beta";
1047         } else {
1048             params.url = fwurl;
1049         }
1050         apiRequest(SHELLYRPC_METHOD_UPDATE, params, String.class);
1051         res.status = "Update initiated";
1052         return res;
1053     }
1054
1055     @Override
1056     public String setCloud(boolean enable) throws ShellyApiException {
1057         Shelly2RpcRequestParams params = new Shelly2RpcRequestParams().withConfig();
1058         params.config.enable = enable;
1059         Shelly2WsConfigResult res = apiRequest(SHELLYRPC_METHOD_CLOUDSET, params, Shelly2WsConfigResult.class);
1060         return res.restartRequired ? "restart required" : "ok";
1061     }
1062
1063     @Override
1064     public String setDebug(boolean enabled) throws ShellyApiException {
1065         return "failed";
1066     }
1067
1068     @Override
1069     public String getDebugLog(String id) throws ShellyApiException {
1070         return ""; // Gen2 uses WS to publish debug log
1071     }
1072
1073     /*
1074      * The following API calls are not yet relevant, because currently there a no Plus/Pro (Gen2) devices of those
1075      * categories (e.g. bulbs)
1076      */
1077
1078     @Override
1079     public void setLightParm(int lightIndex, String parm, String value) throws ShellyApiException {
1080         throw new ShellyApiException("API call not implemented");
1081     }
1082
1083     @Override
1084     public void setLightParms(int lightIndex, Map<String, String> parameters) throws ShellyApiException {
1085         throw new ShellyApiException("API call not implemented");
1086     }
1087
1088     @Override
1089     public void setLightMode(String mode) throws ShellyApiException {
1090         throw new ShellyApiException("API call not implemented");
1091     }
1092
1093     @Override
1094     public void setValveMode(int valveId, boolean auto) throws ShellyApiException {
1095         throw new ShellyApiException("API call not implemented");
1096     }
1097
1098     @Override
1099     public void setValvePosition(int valveId, double value) throws ShellyApiException {
1100         throw new ShellyApiException("API call not implemented");
1101     }
1102
1103     @Override
1104     public void setValveTemperature(int valveId, int value) throws ShellyApiException {
1105         throw new ShellyApiException("API call not implemented");
1106     }
1107
1108     @Override
1109     public void setValveProfile(int valveId, int value) throws ShellyApiException {
1110         throw new ShellyApiException("API call not implemented");
1111     }
1112
1113     @Override
1114     public void setValveBoostTime(int valveId, int value) throws ShellyApiException {
1115         throw new ShellyApiException("API call not implemented");
1116     }
1117
1118     @Override
1119     public void startValveBoost(int valveId, int value) throws ShellyApiException {
1120         throw new ShellyApiException("API call not implemented");
1121     }
1122
1123     @Override
1124     public String resetStaCache() throws ShellyApiException {
1125         throw new ShellyApiException("API call not implemented");
1126     }
1127
1128     @Override
1129     public void setActionURLs() throws ShellyApiException {
1130         // not relevant for Gen2
1131     }
1132
1133     @Override
1134     public ShellySettingsLogin setCoIoTPeer(String peer) throws ShellyApiException {
1135         // not relevant for Gen2
1136         return new ShellySettingsLogin();
1137     }
1138
1139     @Override
1140     public String getCoIoTDescription() {
1141         return ""; // not relevant to Gen2
1142     }
1143
1144     @Override
1145     public void sendIRKey(String keyCode) throws ShellyApiException, IllegalArgumentException {
1146         throw new ShellyApiException("API call not implemented");
1147     }
1148
1149     @Override
1150     public String setWiFiRecovery(boolean enable) throws ShellyApiException {
1151         return "failed"; // not supported by Gen2
1152     }
1153
1154     @Override
1155     public String setApRoaming(boolean enable) throws ShellyApiException {
1156         return "false";// not supported by Gen2
1157     }
1158
1159     private void asyncApiRequest(String method) throws ShellyApiException {
1160         Shelly2RpcBaseMessage request = buildRequest(method, null);
1161         reconnect();
1162         rpcSocket.sendMessage(gson.toJson(request)); // submit, result wull be async
1163     }
1164
1165     @SuppressWarnings("null")
1166     public <T> T apiRequest(String method, @Nullable Object params, Class<T> classOfT) throws ShellyApiException {
1167         String json = "";
1168         Shelly2RpcBaseMessage req = buildRequest(method, params);
1169         try {
1170             reconnect(); // make sure WS is connected
1171             json = rpcPost(gson.toJson(req));
1172         } catch (ShellyApiException e) {
1173             ShellyApiResult res = e.getApiResult();
1174             String auth = getString(res.authChallenge);
1175             if (res.isHttpAccessUnauthorized() && !auth.isEmpty()) {
1176                 String[] options = auth.split(",");
1177                 authInfo = new Shelly2AuthChallenge();
1178                 for (String o : options) {
1179                     String key = substringBefore(o, "=").stripLeading().trim();
1180                     String value = substringAfter(o, "=").replace("\"", "").trim();
1181                     switch (key) {
1182                         case "Digest qop":
1183                             authInfo.authType = SHELLY2_AUTHTTYPE_DIGEST;
1184                             break;
1185                         case "realm":
1186                             authInfo.realm = value;
1187                             break;
1188                         case "nonce":
1189                             // authInfo.nonce = Long.parseLong(value, 16);
1190                             authInfo.nonce = value;
1191                             break;
1192                         case "algorithm":
1193                             authInfo.algorithm = value;
1194                             break;
1195                     }
1196                 }
1197                 json = rpcPost(gson.toJson(req));
1198             } else {
1199                 throw e;
1200             }
1201         }
1202         Shelly2RpcBaseMessage response = gson.fromJson(json, Shelly2RpcBaseMessage.class);
1203         if (response == null) {
1204             throw new IllegalArgumentException("Unable to cover API result to obhect");
1205         }
1206         if (response.result != null) {
1207             // return sub element result as requested class type
1208             json = gson.toJson(gson.fromJson(json, Shelly2RpcBaseMessage.class).result);
1209             boolean isString = response.result instanceof String;
1210             return fromJson(gson, isString && "null".equalsIgnoreCase(((String) response.result)) ? "{}" : json,
1211                     classOfT);
1212         } else {
1213             // return direct format
1214             return gson.fromJson(json, classOfT == String.class ? Shelly2RpcBaseMessage.class : classOfT);
1215         }
1216     }
1217
1218     public <T> T apiRequest(Shelly2RpcRequest request, Class<T> classOfT) throws ShellyApiException {
1219         return apiRequest(request.method, request.params, classOfT);
1220     }
1221
1222     public void apiRequest(Shelly2RpcRequest request) throws ShellyApiException {
1223         apiRequest(request.method, request.params, Shelly2RpcBaseMessage.class);
1224     }
1225
1226     private String rpcPost(String postData) throws ShellyApiException {
1227         return httpPost(authInfo, postData);
1228     }
1229
1230     private void reconnect() throws ShellyApiException {
1231         if (!rpcSocket.isConnected()) {
1232             logger.debug("{}: Connect Rpc Socket (discovery = {})", thingName, discovery);
1233             rpcSocket.connect();
1234         }
1235     }
1236
1237     private void disconnect() {
1238         if (rpcSocket.isConnected()) {
1239             logger.debug("{}: Disconnect Rpc Socket", thingName);
1240         }
1241         rpcSocket.disconnect();
1242     }
1243
1244     public Shelly2RpctInterface getRpcHandler() {
1245         return this;
1246     }
1247
1248     @Override
1249     public void close() {
1250         if (initialized || rpcSocket.isConnected()) {
1251             logger.debug("{}: Closing Rpc API (socket is {}, discovery={})", thingName,
1252                     rpcSocket.isConnected() ? "connected" : "disconnected", discovery);
1253         }
1254         disconnect();
1255         initialized = false;
1256     }
1257
1258     private void incProtErrors() {
1259         if (thing != null) {
1260             thing.incProtErrors();
1261         }
1262     }
1263 }