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