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