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