]> git.basschouten.com Git - openhab-addons.git/blob
b78257d1ef015bbdfab38590d53d2b7335599aa7
[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.ThingStatus;
89 import org.openhab.core.thing.ThingStatusDetail;
90 import org.slf4j.Logger;
91 import org.slf4j.LoggerFactory;
92
93 /**
94  * {@link Shelly2ApiRpc} implements Gen2 RPC interface
95  *
96  * @author Markus Michels - Initial contribution
97  */
98 @NonNullByDefault
99 public class Shelly2ApiRpc extends Shelly2ApiClient implements ShellyApiInterface, Shelly2RpctInterface {
100     private final Logger logger = LoggerFactory.getLogger(Shelly2ApiRpc.class);
101     private final @Nullable ShellyThingTable thingTable;
102
103     protected boolean initialized = false;
104     private boolean discovery = false;
105     private Shelly2RpcSocket rpcSocket = new Shelly2RpcSocket();
106     private @Nullable Shelly2AuthChallenge authInfo;
107
108     /**
109      * Regular constructor - called by Thing handler
110      *
111      * @param thingName Symbolic thing name
112      * @param thing Thing Handler (ThingHandlerInterface)
113      */
114     public Shelly2ApiRpc(String thingName, ShellyThingTable thingTable, ShellyThingInterface thing) {
115         super(thingName, thing);
116         this.thingName = thingName;
117         this.thing = thing;
118         this.thingTable = thingTable;
119     }
120
121     /**
122      * Simple initialization - called by discovery handler
123      *
124      * @param thingName Symbolic thing name
125      * @param config Thing Configuration
126      * @param httpClient HTTP Client to be passed to ShellyHttpClient
127      */
128     public Shelly2ApiRpc(String thingName, ShellyThingConfiguration config, HttpClient httpClient) {
129         super(thingName, config, httpClient);
130         this.thingName = thingName;
131         this.thingTable = null;
132         this.discovery = true;
133     }
134
135     @Override
136     public void initialize() throws ShellyApiException {
137         if (initialized) {
138             logger.debug("{}: Disconnect Rpc Socket on initialize", thingName);
139             disconnect();
140         }
141         rpcSocket = new Shelly2RpcSocket(thingName, thingTable, config.deviceIp);
142         rpcSocket.addMessageHandler(this);
143         initialized = true;
144     }
145
146     @Override
147     public boolean isInitialized() {
148         return initialized;
149     }
150
151     @Override
152     public void startScan() {
153         try {
154             if (getProfile().isBlu) {
155                 installScript(SHELLY2_BLU_GWSCRIPT, config.enableBluGateway);
156             }
157         } catch (ShellyApiException e) {
158         }
159     }
160
161     @SuppressWarnings("null")
162     @Override
163     public ShellyDeviceProfile getDeviceProfile(String thingType, @Nullable ShellySettingsDevice devInfo)
164             throws ShellyApiException {
165         ShellyDeviceProfile profile = thing != null ? getProfile() : new ShellyDeviceProfile();
166
167         if (devInfo != null) {
168             profile.device = devInfo;
169         }
170         if (profile.device.type == null) {
171             profile.device = getDeviceInfo();
172         }
173
174         Shelly2GetConfigResult dc = apiRequest(SHELLYRPC_METHOD_GETCONFIG, null, Shelly2GetConfigResult.class);
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().setThingStatus(ThingStatus.OFFLINE, ThingStatusDetail.FIRMWARE_UPDATING,
688                                 "offline.status-error-fwupgrade");
689                         break;
690                     case SHELLY2_EVENT_OTAPROGRESS:
691                         logger.debug("{}: Firmware update in progress: {}", thingName, getString(e.msg));
692                         break;
693                     case SHELLY2_EVENT_OTADONE:
694                         logger.debug("{}: Firmware update completed with status {}", thingName, getString(e.msg));
695                         getThing().setThingStatus(ThingStatus.OFFLINE, ThingStatusDetail.DUTY_CYCLE,
696                                 "message.offline.status-error-fwcompleted");
697                         break;
698                     case SHELLY2_EVENT_RESTART:
699                         logger.debug("{}: Device was restarted: {}", thingName, getString(e.msg));
700                         getThing().setThingStatus(ThingStatus.OFFLINE, ThingStatusDetail.DUTY_CYCLE,
701                                 "offline.status-error-restarted");
702                         getThing().postEvent(ALARM_TYPE_RESTARTED, true);
703                         break;
704                     case SHELLY2_EVENT_SLEEP:
705                         logger.debug("{}: Connection terminated, e.g. device in sleep mode", thingName);
706                         break;
707                     case SHELLY2_EVENT_WIFICONNFAILED:
708                         logger.debug("{}: WiFi connect failed, check setup, reason {}", thingName,
709                                 getInteger(e.reason));
710                         getThing().postEvent(e.event, false);
711                         break;
712                     case SHELLY2_EVENT_WIFIDISCONNECTED:
713                         logger.debug("{}: WiFi disconnected, reason {}", thingName, getInteger(e.reason));
714                         getThing().postEvent(e.event, false);
715                         break;
716                     default:
717                         logger.debug("{}: Event {} was not handled", thingName, e.event);
718                 }
719             }
720         } catch (ShellyApiException e) {
721             logger.debug("{}: Unable to process event", thingName, e);
722             incProtErrors();
723         }
724     }
725
726     @Override
727     public void onMessage(String message) {
728         logger.debug("{}: Unexpected RPC message received: {}", thingName, message);
729         incProtErrors();
730     }
731
732     @Override
733     public void onClose(int statusCode, String description) {
734         try {
735             String reason = getString(description);
736             logger.debug("{}: WebSocket connection closed, status = {}/{}", thingName, statusCode, reason);
737             if ("Bye".equalsIgnoreCase(reason)) {
738                 logger.debug("{}: Device went to sleep mode or was restarted", thingName);
739             } else if (statusCode == StatusCode.ABNORMAL && !discovery && getProfile().alwaysOn) {
740                 // e.g. device rebooted
741                 if (getThing().getThingStatusDetail() != ThingStatusDetail.DUTY_CYCLE) {
742                     thingOffline("WebSocket connection closed abnormally");
743                 }
744             }
745         } catch (ShellyApiException e) {
746             logger.debug("{}: Exception on onClose()", thingName, e);
747             incProtErrors();
748         }
749     }
750
751     @Override
752     public void onError(Throwable cause) {
753         logger.debug("{}: WebSocket error: {}", thingName, cause.getMessage());
754         if (thing != null && thing.getProfile().alwaysOn) {
755             thingOffline("WebSocket error");
756         }
757     }
758
759     private void thingOffline(String reason) {
760         if (thing != null) { // do not reinit of battery powered devices with sleep mode
761             thing.setThingOfflineAndDisconnect(ThingStatusDetail.COMMUNICATION_ERROR,
762                     "offline.status-error-unexpected-error", reason);
763         }
764     }
765
766     @Override
767     public ShellySettingsDevice getDeviceInfo() throws ShellyApiException {
768         Shelly2DeviceSettings device = callApi("/shelly", Shelly2DeviceSettings.class);
769         ShellySettingsDevice info = new ShellySettingsDevice();
770         info.hostname = getString(device.id);
771         info.name = getString(device.name);
772         info.fw = getString(device.fw);
773         info.type = getString(device.model);
774         info.mac = getString(device.mac);
775         info.auth = getBool(device.auth);
776         info.gen = getInteger(device.gen);
777         info.mode = mapValue(MAP_PROFILE, device.profile);
778         return info;
779     }
780
781     @Override
782     public ShellySettingsStatus getStatus() throws ShellyApiException {
783         ShellyDeviceProfile profile = getProfile();
784         ShellySettingsStatus status = profile.status;
785         Shelly2DeviceStatusResult ds = apiRequest(SHELLYRPC_METHOD_GETSTATUS, null, Shelly2DeviceStatusResult.class);
786         status.time = ds.sys.time;
787         status.uptime = ds.sys.uptime;
788         status.cloud.connected = getBool(ds.cloud.connected);
789         status.mqtt.connected = getBool(ds.mqtt.connected);
790         status.wifiSta.ssid = getString(ds.wifi.ssid);
791         status.wifiSta.enabled = !status.wifiSta.ssid.isEmpty();
792         status.wifiSta.ip = getString(ds.wifi.staIP);
793         status.wifiSta.rssi = getInteger(ds.wifi.rssi);
794         status.fsFree = ds.sys.fsFree;
795         status.fsSize = ds.sys.fsSize;
796         status.discoverable = getBool(profile.settings.discoverable);
797
798         if (ds.sys.wakeupPeriod != null) {
799             profile.settings.sleepMode.period = ds.sys.wakeupPeriod / 60;
800         }
801
802         if (ds.sys.availableUpdates != null) {
803             status.update.hasUpdate = ds.sys.availableUpdates.stable != null;
804             if (ds.sys.availableUpdates.stable != null) {
805                 status.update.newVersion = ShellyDeviceProfile
806                         .extractFwVersion(getString(ds.sys.availableUpdates.stable.version));
807                 status.hasUpdate = new ShellyVersionDTO().compare(profile.fwVersion, status.update.newVersion) < 0;
808             }
809             if (ds.sys.availableUpdates.beta != null) {
810                 status.update.betaVersion = ShellyDeviceProfile
811                         .extractFwVersion(getString(ds.sys.availableUpdates.beta.version));
812                 status.hasUpdate = new ShellyVersionDTO().compare(profile.fwVersion, status.update.betaVersion) < 0;
813             }
814         }
815
816         if (ds.sys.wakeUpReason != null && ds.sys.wakeUpReason.boot != null)
817
818         {
819             List<Object> values = new ArrayList<>();
820             String boot = getString(ds.sys.wakeUpReason.boot);
821             String cause = getString(ds.sys.wakeUpReason.cause);
822
823             // Index 0 is aggregated status, 1 boot, 2 cause
824             String reason = boot.equals(SHELLY2_WAKEUPO_BOOT_RESTART) ? ALARM_TYPE_RESTARTED : cause;
825             values.add(reason);
826             values.add(ds.sys.wakeUpReason.boot);
827             values.add(ds.sys.wakeUpReason.cause);
828             getThing().updateWakeupReason(values);
829         }
830
831         fillDeviceStatus(status, ds, false);
832         if (getBool(profile.settings.rangeExtender)) {
833             try {
834                 // Get List of AP clients
835                 profile.status.rangeExtender = apiRequest(SHELLYRPC_METHOD_WIFILISTAPCLIENTS, null,
836                         Shelly2APClientList.class);
837                 logger.debug("{}: Range extender is enabled, {} clients connected", thingName,
838                         profile.status.rangeExtender.apClients.size());
839             } catch (ShellyApiException e) {
840                 logger.debug("{}: Range extender is enabled, but unable to read AP client list", thingName, e);
841                 profile.settings.rangeExtender = false;
842             }
843         }
844
845         return status;
846     }
847
848     @Override
849     public void setSleepTime(int value) throws ShellyApiException {
850     }
851
852     @Override
853     public ShellyStatusRelay getRelayStatus(int relayIndex) throws ShellyApiException {
854         if (getProfile().status.wifiSta.ssid == null) {
855             // Update status when not yet initialized
856             getStatus();
857         }
858         return relayStatus;
859     }
860
861     @Override
862     public void setRelayTurn(int id, String turnMode) throws ShellyApiException {
863         ShellyDeviceProfile profile = getProfile();
864         int rIdx = id;
865         if (profile.settings.relays != null) {
866             Integer rid = profile.settings.relays.get(id).id;
867             if (rid != null) {
868                 rIdx = rid;
869             }
870         }
871         Shelly2RpcRequestParams params = new Shelly2RpcRequestParams();
872         params.id = rIdx;
873         params.on = SHELLY_API_ON.equals(turnMode);
874         apiRequest(SHELLYRPC_METHOD_SWITCH_SET, params, String.class);
875     }
876
877     @Override
878     public ShellyRollerStatus getRollerStatus(int rollerIndex) throws ShellyApiException {
879         if (rollerIndex < rollerStatus.size()) {
880             return rollerStatus.get(rollerIndex);
881         }
882         throw new IllegalArgumentException("Invalid rollerIndex on getRollerStatus");
883     }
884
885     @Override
886     public void setRollerTurn(int relayIndex, String turnMode) throws ShellyApiException {
887         String operation = "";
888         switch (turnMode) {
889             case SHELLY_ALWD_ROLLER_TURN_OPEN:
890                 operation = SHELLY2_COVER_CMD_OPEN;
891                 break;
892             case SHELLY_ALWD_ROLLER_TURN_CLOSE:
893                 operation = SHELLY2_COVER_CMD_CLOSE;
894                 break;
895             case SHELLY_ALWD_ROLLER_TURN_STOP:
896                 operation = SHELLY2_COVER_CMD_STOP;
897                 break;
898         }
899
900         apiRequest(new Shelly2RpcRequest().withMethod("Cover." + operation).withId(relayIndex));
901     }
902
903     @Override
904     public void setRollerPos(int relayIndex, int position) throws ShellyApiException {
905         apiRequest(
906                 new Shelly2RpcRequest().withMethod(SHELLYRPC_METHOD_COVER_SETPOS).withId(relayIndex).withPos(position));
907     }
908
909     @Override
910     public ShellyStatusLight getLightStatus() throws ShellyApiException {
911         throw new ShellyApiException("API call not implemented");
912     }
913
914     @Override
915     public ShellyShortLightStatus getLightStatus(int index) throws ShellyApiException {
916         ShellyShortLightStatus status = new ShellyShortLightStatus();
917         Shelly2DeviceStatusLight ls = apiRequest(
918                 new Shelly2RpcRequest().withMethod(SHELLYRPC_METHOD_LIGHT_STATUS).withId(index),
919                 Shelly2DeviceStatusLight.class);
920         status.ison = ls.output;
921         status.hasTimer = ls.timerStartedAt != null;
922         status.timerDuration = getDuration(ls.timerStartedAt, ls.timerDuration);
923         if (ls.brightness != null) {
924             status.brightness = ls.brightness.intValue();
925         }
926         return status;
927     }
928
929     @Override
930     public void setBrightness(int id, int brightness, boolean autoOn) throws ShellyApiException {
931         Shelly2RpcRequestParams params = new Shelly2RpcRequestParams();
932         params.id = id;
933         params.brightness = brightness;
934         params.on = brightness > 0;
935         apiRequest(SHELLYRPC_METHOD_LIGHT_SET, params, String.class);
936     }
937
938     @Override
939     public ShellyShortLightStatus setLightTurn(int id, String turnMode) throws ShellyApiException {
940         Shelly2RpcRequestParams params = new Shelly2RpcRequestParams();
941         params.id = id;
942         params.on = turnMode.equals(SHELLY_API_ON);
943         apiRequest(SHELLYRPC_METHOD_LIGHT_SET, params, String.class);
944         return getLightStatus(id);
945     }
946
947     @Override
948     public ShellyStatusSensor getSensorStatus() throws ShellyApiException {
949         return sensorData;
950     }
951
952     @Override
953     public void setAutoTimer(int index, String timerName, double value) throws ShellyApiException {
954         ShellyDeviceProfile profile = getProfile();
955         boolean isLight = profile.isLight || profile.isDimmer;
956         String method = isLight ? SHELLYRPC_METHOD_LIGHT_SETCONFIG : SHELLYRPC_METHOD_SWITCH_SETCONFIG;
957         String component = isLight ? "Light" : "Switch";
958         Shelly2RpcRequest req = new Shelly2RpcRequest().withMethod(method).withId(index);
959         req.params.withConfig();
960         req.params.config.name = component + index;
961         if (timerName.equals(SHELLY_TIMER_AUTOON)) {
962             req.params.config.autoOn = value > 0;
963             req.params.config.autoOnDelay = value;
964         } else {
965             req.params.config.autoOff = value > 0;
966             req.params.config.autoOffDelay = value;
967         }
968         apiRequest(req);
969     }
970
971     @Override
972     public void setLedStatus(String ledName, boolean value) throws ShellyApiException {
973         Shelly2RpcRequestParams params = new Shelly2RpcRequestParams().withConfig();
974         params.id = 0;
975         if (ledName.equals(SHELLY_LED_STATUS_DISABLE)) {
976             params.config.sysLedEnable = value;
977         } else if (ledName.equals(SHELLY_LED_POWER_DISABLE)) {
978             params.config.powerLed = value ? SHELLY2_POWERLED_OFF : SHELLY2_POWERLED_MATCH;
979         } else {
980             throw new ShellyApiException("API call not implemented for this LED type");
981         }
982         apiRequest(SHELLYRPC_METHOD_LED_SETCONFIG, params, Shelly2WsConfigResult.class);
983     }
984
985     @Override
986     public void resetMeterTotal(int id) throws ShellyApiException {
987         apiRequest(new Shelly2RpcRequest()
988                 .withMethod(getProfile().is3EM ? SHELLYRPC_METHOD_EMDATARESET : SHELLYRPC_METHOD_EM1DATARESET)
989                 .withId(id));
990     }
991
992     @Override
993     public void muteSmokeAlarm(int index) throws ShellyApiException {
994         apiRequest(new Shelly2RpcRequest().withMethod(SHELLYRPC_METHOD_SMOKE_MUTE).withId(index));
995     }
996
997     @Override
998     public ShellySettingsLogin getLoginSettings() throws ShellyApiException {
999         return new ShellySettingsLogin();
1000     }
1001
1002     @Override
1003     public ShellySettingsLogin setLoginCredentials(String user, String password) throws ShellyApiException {
1004         Shelly2RpcRequestParams params = new Shelly2RpcRequestParams();
1005         params.user = "admin";
1006         params.realm = config.serviceName;
1007         params.ha1 = sha256(params.user + ":" + params.realm + ":" + password);
1008         apiRequest(SHELLYRPC_METHOD_AUTHSET, params, String.class);
1009
1010         ShellySettingsLogin res = new ShellySettingsLogin();
1011         res.enabled = true;
1012         res.username = params.user;
1013         res.password = password;
1014         return new ShellySettingsLogin();
1015     }
1016
1017     @Override
1018     public boolean setWiFiRangeExtender(boolean enable) throws ShellyApiException {
1019         Shelly2RpcRequestParams params = new Shelly2RpcRequestParams().withConfig();
1020         params.config.ap = new Shelly2DeviceConfigAp();
1021         params.config.ap.rangeExtender = new Shelly2DeviceConfigApRE();
1022         params.config.ap.rangeExtender.enable = enable;
1023         Shelly2WsConfigResult res = apiRequest(SHELLYRPC_METHOD_WIFISETCONG, params, Shelly2WsConfigResult.class);
1024         return res.restartRequired;
1025     }
1026
1027     @Override
1028     public boolean setEthernet(boolean enable) throws ShellyApiException {
1029         Shelly2RpcRequestParams params = new Shelly2RpcRequestParams().withConfig();
1030         params.config.enable = enable;
1031         Shelly2WsConfigResult res = apiRequest(SHELLYRPC_METHOD_ETHSETCONG, params, Shelly2WsConfigResult.class);
1032         return res.restartRequired;
1033     }
1034
1035     @Override
1036     public boolean setBluetooth(boolean enable) throws ShellyApiException {
1037         Shelly2RpcRequestParams params = new Shelly2RpcRequestParams().withConfig();
1038         params.config.enable = enable;
1039         if (enable) {
1040             params.config.observer = new Shelly2DevConfigBleObserver();
1041             params.config.observer.enable = false;
1042         }
1043         Shelly2WsConfigResult res = apiRequest(SHELLYRPC_METHOD_BLESETCONG, params, Shelly2WsConfigResult.class);
1044         return res.restartRequired;
1045     }
1046
1047     @Override
1048     public void deviceReboot() throws ShellyApiException {
1049         apiRequest(SHELLYRPC_METHOD_REBOOT, null, String.class);
1050     }
1051
1052     @Override
1053     public String factoryReset() throws ShellyApiException {
1054         return apiRequest(SHELLYRPC_METHOD_RESET, null, String.class);
1055     }
1056
1057     @Override
1058     public ShellyOtaCheckResult checkForUpdate() throws ShellyApiException {
1059         Shelly2DeviceStatusSysAvlUpdate status = apiRequest(SHELLYRPC_METHOD_CHECKUPD, null,
1060                 Shelly2DeviceStatusSysAvlUpdate.class);
1061         ShellyOtaCheckResult result = new ShellyOtaCheckResult();
1062         result.status = status.stable != null || status.beta != null ? "new" : "ok";
1063         return result;
1064     }
1065
1066     @Override
1067     public ShellySettingsUpdate firmwareUpdate(String fwurl) throws ShellyApiException {
1068         ShellySettingsUpdate res = new ShellySettingsUpdate();
1069         boolean prod = fwurl.contains("update");
1070         boolean beta = fwurl.contains("beta");
1071
1072         Shelly2RpcRequestParams params = new Shelly2RpcRequestParams();
1073         if (prod || beta) {
1074             params.stage = prod ? "stable" : "beta";
1075         } else {
1076             params.url = fwurl;
1077         }
1078         apiRequest(SHELLYRPC_METHOD_UPDATE, params, String.class);
1079         res.status = "Update initiated";
1080         return res;
1081     }
1082
1083     @Override
1084     public String setCloud(boolean enable) throws ShellyApiException {
1085         Shelly2RpcRequestParams params = new Shelly2RpcRequestParams().withConfig();
1086         params.config.enable = enable;
1087         Shelly2WsConfigResult res = apiRequest(SHELLYRPC_METHOD_CLOUDSET, params, Shelly2WsConfigResult.class);
1088         return res.restartRequired ? "restart required" : "ok";
1089     }
1090
1091     @Override
1092     public String setDebug(boolean enabled) throws ShellyApiException {
1093         return "failed";
1094     }
1095
1096     @Override
1097     public String getDebugLog(String id) throws ShellyApiException {
1098         return ""; // Gen2 uses WS to publish debug log
1099     }
1100
1101     /*
1102      * The following API calls are not yet relevant, because currently there a no Plus/Pro (Gen2) devices of those
1103      * categories (e.g. bulbs)
1104      */
1105
1106     @Override
1107     public void setLightParm(int lightIndex, String parm, String value) throws ShellyApiException {
1108         throw new ShellyApiException("API call not implemented");
1109     }
1110
1111     @Override
1112     public void setLightParms(int lightIndex, Map<String, String> parameters) throws ShellyApiException {
1113         throw new ShellyApiException("API call not implemented");
1114     }
1115
1116     @Override
1117     public void setLightMode(String mode) throws ShellyApiException {
1118         throw new ShellyApiException("API call not implemented");
1119     }
1120
1121     @Override
1122     public void setValveMode(int valveId, boolean auto) throws ShellyApiException {
1123         throw new ShellyApiException("API call not implemented");
1124     }
1125
1126     @Override
1127     public void setValvePosition(int valveId, double value) throws ShellyApiException {
1128         throw new ShellyApiException("API call not implemented");
1129     }
1130
1131     @Override
1132     public void setValveTemperature(int valveId, double value) throws ShellyApiException {
1133         throw new ShellyApiException("API call not implemented");
1134     }
1135
1136     @Override
1137     public void setValveProfile(int valveId, int value) throws ShellyApiException {
1138         throw new ShellyApiException("API call not implemented");
1139     }
1140
1141     @Override
1142     public void setValveBoostTime(int valveId, int value) throws ShellyApiException {
1143         throw new ShellyApiException("API call not implemented");
1144     }
1145
1146     @Override
1147     public void startValveBoost(int valveId, int value) throws ShellyApiException {
1148         throw new ShellyApiException("API call not implemented");
1149     }
1150
1151     @Override
1152     public String resetStaCache() throws ShellyApiException {
1153         throw new ShellyApiException("API call not implemented");
1154     }
1155
1156     @Override
1157     public void setActionURLs() throws ShellyApiException {
1158         // not relevant for Gen2
1159     }
1160
1161     @Override
1162     public ShellySettingsLogin setCoIoTPeer(String peer) throws ShellyApiException {
1163         // not relevant for Gen2
1164         return new ShellySettingsLogin();
1165     }
1166
1167     @Override
1168     public String getCoIoTDescription() {
1169         return ""; // not relevant to Gen2
1170     }
1171
1172     @Override
1173     public void sendIRKey(String keyCode) throws ShellyApiException, IllegalArgumentException {
1174         throw new ShellyApiException("API call not implemented");
1175     }
1176
1177     @Override
1178     public String setWiFiRecovery(boolean enable) throws ShellyApiException {
1179         return "failed"; // not supported by Gen2
1180     }
1181
1182     @Override
1183     public String setApRoaming(boolean enable) throws ShellyApiException {
1184         return "false";// not supported by Gen2
1185     }
1186
1187     private void asyncApiRequest(String method) throws ShellyApiException {
1188         Shelly2RpcBaseMessage request = buildRequest(method, null);
1189         reconnect();
1190         rpcSocket.sendMessage(gson.toJson(request)); // submit, result wull be async
1191     }
1192
1193     @SuppressWarnings("null")
1194     public <T> T apiRequest(String method, @Nullable Object params, Class<T> classOfT) throws ShellyApiException {
1195         String json = "";
1196         Shelly2RpcBaseMessage req = buildRequest(method, params);
1197         try {
1198             reconnect(); // make sure WS is connected
1199             json = rpcPost(gson.toJson(req));
1200         } catch (ShellyApiException e) {
1201             ShellyApiResult res = e.getApiResult();
1202             String auth = getString(res.authChallenge);
1203             if (res.isHttpAccessUnauthorized() && !auth.isEmpty()) {
1204                 String[] options = auth.split(",");
1205                 authInfo = new Shelly2AuthChallenge();
1206                 for (String o : options) {
1207                     String key = substringBefore(o, "=").stripLeading().trim();
1208                     String value = substringAfter(o, "=").replace("\"", "").trim();
1209                     switch (key) {
1210                         case "Digest qop":
1211                             authInfo.authType = SHELLY2_AUTHTTYPE_DIGEST;
1212                             break;
1213                         case "realm":
1214                             authInfo.realm = value;
1215                             break;
1216                         case "nonce":
1217                             // authInfo.nonce = Long.parseLong(value, 16);
1218                             authInfo.nonce = value;
1219                             break;
1220                         case "algorithm":
1221                             authInfo.algorithm = value;
1222                             break;
1223                     }
1224                 }
1225                 json = rpcPost(gson.toJson(req));
1226             } else {
1227                 throw e;
1228             }
1229         }
1230         Shelly2RpcBaseMessage response = gson.fromJson(json, Shelly2RpcBaseMessage.class);
1231         if (response == null) {
1232             throw new IllegalArgumentException("Unable to cover API result to obhect");
1233         }
1234         if (response.result != null) {
1235             // return sub element result as requested class type
1236             json = gson.toJson(gson.fromJson(json, Shelly2RpcBaseMessage.class).result);
1237             boolean isString = response.result instanceof String;
1238             return fromJson(gson, isString && "null".equalsIgnoreCase(((String) response.result)) ? "{}" : json,
1239                     classOfT);
1240         } else {
1241             // return direct format
1242             return gson.fromJson(json, classOfT == String.class ? Shelly2RpcBaseMessage.class : classOfT);
1243         }
1244     }
1245
1246     public <T> T apiRequest(Shelly2RpcRequest request, Class<T> classOfT) throws ShellyApiException {
1247         return apiRequest(request.method, request.params, classOfT);
1248     }
1249
1250     public void apiRequest(Shelly2RpcRequest request) throws ShellyApiException {
1251         apiRequest(request.method, request.params, Shelly2RpcBaseMessage.class);
1252     }
1253
1254     private String rpcPost(String postData) throws ShellyApiException {
1255         return httpPost(authInfo, postData);
1256     }
1257
1258     private void reconnect() throws ShellyApiException {
1259         if (!rpcSocket.isConnected()) {
1260             logger.debug("{}: Connect Rpc Socket (discovery = {})", thingName, discovery);
1261             rpcSocket.connect();
1262         }
1263     }
1264
1265     private void disconnect() {
1266         if (rpcSocket.isConnected()) {
1267             logger.debug("{}: Disconnect Rpc Socket", thingName);
1268         }
1269         rpcSocket.disconnect();
1270     }
1271
1272     public Shelly2RpctInterface getRpcHandler() {
1273         return this;
1274     }
1275
1276     @Override
1277     public void close() {
1278         if (initialized || rpcSocket.isConnected()) {
1279             logger.debug("{}: Closing Rpc API (socket is {}, discovery={})", thingName,
1280                     rpcSocket.isConnected() ? "connected" : "disconnected", discovery);
1281         }
1282         disconnect();
1283         initialized = false;
1284     }
1285
1286     private void incProtErrors() {
1287         if (thing != null) {
1288             thing.incProtErrors();
1289         }
1290     }
1291 }