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