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