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