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