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