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