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