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