]> git.basschouten.com Git - openhab-addons.git/blob
c054b9256d7bd44dc56acd3c2589edd794853a0d
[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.util.ArrayList;
21 import java.util.List;
22 import java.util.Map;
23
24 import org.eclipse.jdt.annotation.NonNullByDefault;
25 import org.eclipse.jdt.annotation.Nullable;
26 import org.eclipse.jetty.client.HttpClient;
27 import org.eclipse.jetty.http.HttpStatus;
28 import org.eclipse.jetty.websocket.api.StatusCode;
29 import org.openhab.binding.shelly.internal.api.ShellyApiException;
30 import org.openhab.binding.shelly.internal.api.ShellyApiInterface;
31 import org.openhab.binding.shelly.internal.api.ShellyApiResult;
32 import org.openhab.binding.shelly.internal.api.ShellyDeviceProfile;
33 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyInputState;
34 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyOtaCheckResult;
35 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyRollerStatus;
36 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySensorSleepMode;
37 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsDevice;
38 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsEMeter;
39 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsLogin;
40 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsMeter;
41 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsRelay;
42 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsStatus;
43 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsUpdate;
44 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsWiFiNetwork;
45 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyShortLightStatus;
46 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyShortStatusRelay;
47 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyStatusLight;
48 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyStatusRelay;
49 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyStatusSensor;
50 import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2AuthResponse;
51 import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2ConfigParms;
52 import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2DeviceConfig.Shelly2DeviceConfigSta;
53 import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2DeviceConfig.Shelly2GetConfigResult;
54 import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2DeviceConfigAp;
55 import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2DeviceConfigAp.Shelly2DeviceConfigApRE;
56 import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2DeviceSettings;
57 import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2DeviceStatus.Shelly2DeviceStatusResult;
58 import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2DeviceStatus.Shelly2DeviceStatusSys.Shelly2DeviceStatusSysAvlUpdate;
59 import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2NotifyEvent;
60 import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2RpcBaseMessage;
61 import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2RpcNotifyEvent;
62 import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2RpcNotifyStatus;
63 import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2RpcNotifyStatus.Shelly2NotifyStatus;
64 import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2RpcRequest;
65 import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2RpcRequest.Shelly2RpcRequestParams;
66 import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2WsConfigResponse;
67 import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2WsConfigResponse.Shelly2WsConfigResult;
68 import org.openhab.binding.shelly.internal.config.ShellyThingConfiguration;
69 import org.openhab.binding.shelly.internal.handler.ShellyThingInterface;
70 import org.openhab.binding.shelly.internal.handler.ShellyThingTable;
71 import org.openhab.core.library.unit.SIUnits;
72 import org.openhab.core.thing.ThingStatusDetail;
73 import org.slf4j.Logger;
74 import org.slf4j.LoggerFactory;
75
76 /**
77  * {@link Shelly2ApiRpc} implements Gen2 RPC interface
78  *
79  * @author Markus Michels - Initial contribution
80  */
81 @NonNullByDefault
82 public class Shelly2ApiRpc extends Shelly2ApiClient implements ShellyApiInterface, Shelly2RpctInterface {
83     private final Logger logger = LoggerFactory.getLogger(Shelly2ApiRpc.class);
84     private final @Nullable ShellyThingTable thingTable;
85
86     private boolean initialized = false;
87     private boolean discovery = false;
88     private Shelly2RpcSocket rpcSocket = new Shelly2RpcSocket();
89     private Shelly2AuthResponse authInfo = new Shelly2AuthResponse();
90
91     /**
92      * Regular constructor - called by Thing handler
93      *
94      * @param thingName Symbolic thing name
95      * @param thing Thing Handler (ThingHandlerInterface)
96      */
97     public Shelly2ApiRpc(String thingName, ShellyThingTable thingTable, ShellyThingInterface thing) {
98         super(thingName, thing);
99         this.thingName = thingName;
100         this.thing = thing;
101         this.thingTable = thingTable;
102         try {
103             getProfile().initFromThingType(thing.getThingType());
104         } catch (ShellyApiException e) {
105             logger.info("{}: Shelly2 API initialization failed!", thingName, e);
106         }
107     }
108
109     /**
110      * Simple initialization - called by discovery handler
111      *
112      * @param thingName Symbolic thing name
113      * @param config Thing Configuration
114      * @param httpClient HTTP Client to be passed to ShellyHttpClient
115      */
116     public Shelly2ApiRpc(String thingName, ShellyThingConfiguration config, HttpClient httpClient) {
117         super(thingName, config, httpClient);
118         this.thingName = thingName;
119         this.thingTable = null;
120         this.discovery = true;
121     }
122
123     @Override
124     public void initialize() throws ShellyApiException {
125         if (!initialized) {
126             rpcSocket = new Shelly2RpcSocket(thingName, thingTable, config.deviceIp);
127             rpcSocket.addMessageHandler(this);
128             initialized = true;
129         } else {
130             if (rpcSocket.isConnected()) {
131                 logger.debug("{}: Disconnect Rpc Socket on initialize", thingName);
132                 disconnect();
133             }
134         }
135     }
136
137     @Override
138     public boolean isInitialized() {
139         return initialized;
140     }
141
142     @Override
143     public ShellyDeviceProfile getDeviceProfile(String thingType) throws ShellyApiException {
144         ShellyDeviceProfile profile = thing != null ? getProfile() : new ShellyDeviceProfile();
145
146         Shelly2GetConfigResult dc = apiRequest(SHELLYRPC_METHOD_GETCONFIG, null, Shelly2GetConfigResult.class);
147         profile.isGen2 = true;
148         profile.settingsJson = gson.toJson(dc);
149         profile.thingName = thingName;
150         profile.settings.name = profile.status.name = dc.sys.device.name;
151         profile.name = getString(profile.settings.name);
152         profile.settings.timezone = getString(dc.sys.location.tz);
153         profile.settings.discoverable = getBool(dc.sys.device.discoverable);
154         if (dc.wifi != null && dc.wifi.ap != null && dc.wifi.ap.rangeExtender != null) {
155             profile.settings.wifiAp.rangeExtender = getBool(dc.wifi.ap.rangeExtender.enable);
156         }
157         if (dc.cloud != null) {
158             profile.settings.cloud.enabled = getBool(dc.cloud.enable);
159         }
160         if (dc.mqtt != null) {
161             profile.settings.mqtt.enable = getBool(dc.mqtt.enable);
162         }
163         if (dc.sys.sntp != null) {
164             profile.settings.sntp.server = dc.sys.sntp.server;
165         }
166
167         profile.isRoller = dc.cover0 != null;
168         profile.settings.relays = fillRelaySettings(profile, dc);
169         profile.settings.inputs = fillInputSettings(profile, dc);
170         profile.settings.rollers = fillRollerSettings(profile, dc);
171
172         profile.isEMeter = true;
173         profile.numInputs = profile.settings.inputs != null ? profile.settings.inputs.size() : 0;
174         profile.numRelays = profile.settings.relays != null ? profile.settings.relays.size() : 0;
175         profile.numRollers = profile.settings.rollers != null ? profile.settings.rollers.size() : 0;
176         profile.hasRelays = profile.numRelays > 0 || profile.numRollers > 0;
177         profile.mode = "";
178         if (profile.hasRelays) {
179             profile.mode = profile.isRoller ? SHELLY_CLASS_ROLLER : SHELLY_CLASS_RELAY;
180         }
181
182         ShellySettingsDevice device = getDeviceInfo();
183         profile.settings.device = device;
184         profile.hostname = device.hostname;
185         profile.deviceType = device.type;
186         profile.mac = device.mac;
187         profile.auth = device.auth;
188         if (config.serviceName.isEmpty()) {
189             config.serviceName = getString(profile.hostname);
190         }
191         profile.fwDate = substringBefore(device.fw, "/");
192         profile.fwVersion = substringBefore(ShellyDeviceProfile.extractFwVersion(device.fw.replace("/", "/v")), "-");
193         profile.status.update.oldVersion = profile.fwVersion;
194         profile.status.hasUpdate = profile.status.update.hasUpdate = false;
195
196         if (dc.eth != null) {
197             profile.settings.ethernet = getBool(dc.eth.enable);
198         }
199         if (dc.ble != null) {
200             profile.settings.bluetooth = getBool(dc.ble.enable);
201         }
202
203         profile.settings.wifiSta = new ShellySettingsWiFiNetwork();
204         profile.settings.wifiSta1 = new ShellySettingsWiFiNetwork();
205         fillWiFiSta(dc.wifi.sta, profile.settings.wifiSta);
206         fillWiFiSta(dc.wifi.sta1, profile.settings.wifiSta1);
207
208         if (profile.hasRelays) {
209             profile.status.relays = new ArrayList<>();
210             profile.status.meters = new ArrayList<>();
211             profile.status.emeters = new ArrayList<>();
212             relayStatus.relays = new ArrayList<>();
213             relayStatus.meters = new ArrayList<>();
214             profile.numMeters = profile.isRoller ? profile.numRollers : profile.numRelays;
215             for (int i = 0; i < profile.numRelays; i++) {
216                 profile.status.relays.add(new ShellySettingsRelay());
217                 relayStatus.relays.add(new ShellyShortStatusRelay());
218             }
219             for (int i = 0; i < profile.numMeters; i++) {
220                 profile.status.meters.add(new ShellySettingsMeter());
221                 profile.status.emeters.add(new ShellySettingsEMeter());
222                 relayStatus.meters.add(new ShellySettingsMeter());
223             }
224         }
225
226         if (profile.numInputs > 0) {
227             profile.status.inputs = new ArrayList<>();
228             relayStatus.inputs = new ArrayList<>();
229             for (int i = 0; i < profile.numInputs; i++) {
230                 ShellyInputState input = new ShellyInputState();
231                 input.input = 0;
232                 input.event = "";
233                 input.eventCount = 0;
234                 profile.status.inputs.add(input);
235                 relayStatus.inputs.add(input);
236             }
237         }
238
239         if (profile.isRoller) {
240             profile.status.rollers = new ArrayList<>();
241             for (int i = 0; i < profile.numRollers; i++) {
242                 ShellyRollerStatus rs = new ShellyRollerStatus();
243                 profile.status.rollers.add(rs);
244                 rollerStatus.add(rs);
245             }
246         }
247
248         profile.status.dimmers = profile.isDimmer ? new ArrayList<>() : null;
249         profile.status.lights = profile.isBulb ? new ArrayList<>() : null;
250         profile.status.thermostats = profile.isTRV ? new ArrayList<>() : null;
251
252         if (profile.hasBattery) {
253             profile.settings.sleepMode = new ShellySensorSleepMode();
254             profile.settings.sleepMode.unit = "m";
255             profile.settings.sleepMode.period = dc.sys.sleep != null ? dc.sys.sleep.wakeupPeriod / 60 : 720;
256             checkSetWsCallback();
257         }
258
259         profile.initialized = true;
260         if (!discovery) {
261             getStatus(); // make sure profile.status is initialized (e.g,. relay/meter status)
262             asyncApiRequest(SHELLYRPC_METHOD_GETSTATUS); // request periodic status updates from device
263         }
264
265         return profile;
266     }
267
268     private void fillWiFiSta(@Nullable Shelly2DeviceConfigSta from, ShellySettingsWiFiNetwork to) {
269         to.enabled = from != null && !getString(from.ssid).isEmpty();
270         if (from != null) {
271             to.ssid = from.ssid;
272             to.ip = from.ip;
273             to.mask = from.netmask;
274             to.dns = from.nameserver;
275         }
276     }
277
278     private void checkSetWsCallback() throws ShellyApiException {
279         Shelly2ConfigParms wsConfig = apiRequest(SHELLYRPC_METHOD_WSGETCONFIG, null, Shelly2ConfigParms.class);
280         String url = "ws://" + config.localIp + ":" + config.localPort + "/shelly/wsevent";
281         if (!getBool(wsConfig.enable) || !url.equalsIgnoreCase(getString(wsConfig.server))) {
282             logger.debug("{}: A battery device was detected without correct callback, fix it", thingName);
283             wsConfig.enable = true;
284             wsConfig.server = url;
285             Shelly2RpcRequest request = new Shelly2RpcRequest();
286             request.id = 0;
287             request.method = SHELLYRPC_METHOD_WSSETCONFIG;
288             request.params.config = wsConfig;
289             Shelly2WsConfigResponse response = apiRequest(SHELLYRPC_METHOD_WSSETCONFIG, request.params,
290                     Shelly2WsConfigResponse.class);
291             if (response.result != null && response.result.restartRequired) {
292                 logger.info("{}: WebSocket callback was updated, device is restarting", thingName);
293                 getThing().getApi().deviceReboot();
294                 getThing().reinitializeThing();
295             }
296         }
297     }
298
299     @Override
300     public void onConnect(String deviceIp, boolean connected) {
301         if (thing == null && thingTable != null) {
302             thing = thingTable.getThing(deviceIp);
303             logger.debug("{}: Get thing from thingTable", thingName);
304         }
305     }
306
307     @Override
308     public void onNotifyStatus(Shelly2RpcNotifyStatus message) {
309         logger.debug("{}: NotifyStatus update received: {}", thingName, gson.toJson(message));
310         try {
311             ShellyThingInterface t = thing;
312             if (t == null) {
313                 logger.debug("{}: No matching thing on NotifyStatus for {}, ignore (src={}, dst={}, discovery={})",
314                         thingName, thingName, message.src, message.dst, discovery);
315                 return;
316             }
317             if (!t.isThingOnline() && t.getThingStatusDetail() != ThingStatusDetail.CONFIGURATION_PENDING) {
318                 logger.debug("{}: Thing is not in online state/connectable, ignore NotifyStatus", thingName);
319                 return;
320             }
321
322             getThing().incProtMessages();
323             if (message.error != null) {
324                 if (message.error.code == HttpStatus.UNAUTHORIZED_401 && !getString(message.error.message).isEmpty()) {
325                     // Save nonce for notification
326                     Shelly2AuthResponse auth = gson.fromJson(message.error.message, Shelly2AuthResponse.class);
327                     if (auth != null && auth.realm == null) {
328                         logger.debug("{}: Authentication data received: {}", thingName, message.error.message);
329                         authInfo = auth;
330                     }
331                 } else {
332                     logger.debug("{}: Error status received - {} {}", thingName, message.error.code,
333                             message.error.message);
334                     incProtErrors();
335                 }
336             }
337
338             Shelly2NotifyStatus params = message.params;
339             if (params != null) {
340                 if (getThing().getThingStatusDetail() != ThingStatusDetail.FIRMWARE_UPDATING) {
341                     getThing().setThingOnline();
342                 }
343
344                 boolean updated = false;
345                 ShellyDeviceProfile profile = getProfile();
346                 ShellySettingsStatus status = profile.status;
347                 if (params.sys != null) {
348                     if (getBool(params.sys.restartRequired)) {
349                         logger.warn("{}: Device requires restart to activate changes", thingName);
350                     }
351                     status.uptime = params.sys.uptime;
352                 }
353                 status.temperature = SHELLY_API_INVTEMP; // mark invalid
354                 updated |= fillDeviceStatus(status, message.params, true);
355                 if (getDouble(status.temperature) == SHELLY_API_INVTEMP) {
356                     // no device temp available
357                     status.temperature = null;
358                 } else {
359                     updated |= updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_ITEMP,
360                             toQuantityType(getDouble(status.tmp.tC), DIGITS_NONE, SIUnits.CELSIUS));
361                 }
362
363                 profile.status = status;
364                 if (updated) {
365                     getThing().restartWatchdog();
366                 }
367             }
368         } catch (ShellyApiException e) {
369             logger.debug("{}: Unable to process status update", thingName, e);
370             incProtErrors();
371         }
372     }
373
374     @Override
375     public void onNotifyEvent(Shelly2RpcNotifyEvent message) {
376         try {
377             logger.debug("{}: NotifyEvent  received: {}", thingName, gson.toJson(message));
378             ShellyDeviceProfile profile = getProfile();
379
380             getThing().incProtMessages();
381             getThing().restartWatchdog();
382
383             for (Shelly2NotifyEvent e : message.params.events) {
384                 switch (e.event) {
385                     case SHELLY2_EVENT_BTNUP:
386                     case SHELLY2_EVENT_BTNDOWN:
387                         String bgroup = getProfile().getInputGroup(e.id);
388                         updateChannel(bgroup, CHANNEL_INPUT + profile.getInputSuffix(e.id),
389                                 getOnOff(SHELLY2_EVENT_BTNDOWN.equals(getString(e.event))));
390                         getThing().triggerButton(profile.getInputGroup(e.id), e.id,
391                                 mapValue(MAP_INPUT_EVENT_ID, e.event));
392                         break;
393
394                     case SHELLY2_EVENT_1PUSH:
395                     case SHELLY2_EVENT_2PUSH:
396                     case SHELLY2_EVENT_3PUSH:
397                     case SHELLY2_EVENT_LPUSH:
398                     case SHELLY2_EVENT_SLPUSH:
399                     case SHELLY2_EVENT_LSPUSH:
400                         if (e.id < profile.numInputs) {
401                             ShellyInputState input = relayStatus.inputs.get(e.id);
402                             input.event = getString(MAP_INPUT_EVENT_TYPE.get(e.event));
403                             input.eventCount = getInteger(input.eventCount) + 1;
404                             relayStatus.inputs.set(e.id, input);
405                             profile.status.inputs.set(e.id, input);
406
407                             String group = getProfile().getInputGroup(e.id);
408                             updateChannel(group, CHANNEL_STATUS_EVENTTYPE + profile.getInputSuffix(e.id),
409                                     getStringType(input.event));
410                             updateChannel(group, CHANNEL_STATUS_EVENTCOUNT + profile.getInputSuffix(e.id),
411                                     getDecimal(input.eventCount));
412                             getThing().triggerButton(profile.getInputGroup(e.id), e.id,
413                                     mapValue(MAP_INPUT_EVENT_ID, e.event));
414                         }
415                         break;
416                     case SHELLY2_EVENT_CFGCHANGED:
417                         logger.debug("{}: Configuration update detected, re-initialize", thingName);
418                         getThing().requestUpdates(1, true); // refresh config
419                         break;
420
421                     case SHELLY2_EVENT_OTASTART:
422                         logger.debug("{}: Firmware update started: {}", thingName, getString(e.msg));
423                         getThing().postEvent(e.event, true);
424                         getThing().setThingOffline(ThingStatusDetail.FIRMWARE_UPDATING,
425                                 "offline.status-error-fwupgrade");
426                         break;
427                     case SHELLY2_EVENT_OTAPROGRESS:
428                         logger.debug("{}: Firmware update in progress: {}", thingName, getString(e.msg));
429                         getThing().postEvent(e.event, false);
430                         break;
431                     case SHELLY2_EVENT_OTADONE:
432                         logger.debug("{}: Firmware update completed: {}", thingName, getString(e.msg));
433                         getThing().setThingOffline(ThingStatusDetail.CONFIGURATION_PENDING,
434                                 "offline.status-error-restarted");
435                         getThing().requestUpdates(1, true); // refresh config
436                         break;
437                     case SHELLY2_EVENT_SLEEP:
438                         logger.debug("{}: Device went to sleep mode", thingName);
439                         break;
440                     case SHELLY2_EVENT_WIFICONNFAILED:
441                         logger.debug("{}: WiFi connect failed, check setup, reason {}", thingName,
442                                 getInteger(e.reason));
443                         getThing().postEvent(e.event, false);
444                         break;
445                     case SHELLY2_EVENT_WIFIDISCONNECTED:
446                         logger.debug("{}: WiFi disconnected, reason {}", thingName, getInteger(e.reason));
447                         getThing().postEvent(e.event, false);
448                         break;
449                     default:
450                         logger.debug("{}: Event {} was not handled", thingName, e.event);
451                 }
452             }
453         } catch (ShellyApiException e) {
454             logger.debug("{}: Unable to process event", thingName, e);
455             incProtErrors();
456         }
457     }
458
459     @Override
460     public void onMessage(String message) {
461         logger.debug("{}: Unexpected RPC message received: {}", thingName, message);
462         incProtErrors();
463     }
464
465     @Override
466     public void onClose(int statusCode, String reason) {
467         try {
468             logger.debug("{}: WebSocket connection closed, status = {}/{}", thingName, statusCode, getString(reason));
469             if (statusCode == StatusCode.ABNORMAL && !discovery && getProfile().alwaysOn) { // e.g. device rebooted
470                 thingOffline("WebSocket connection closed abnormal");
471             }
472         } catch (ShellyApiException e) {
473             logger.debug("{}: Exception on onClose()", thingName, e);
474             incProtErrors();
475         }
476     }
477
478     @Override
479     public void onError(Throwable cause) {
480         logger.debug("{}: WebSocket error", thingName);
481         if (thing != null && thing.getProfile().alwaysOn) {
482             thingOffline("WebSocket error");
483         }
484     }
485
486     private void thingOffline(String reason) {
487         if (thing != null) { // do not reinit of battery powered devices with sleep mode
488             thing.setThingOffline(ThingStatusDetail.COMMUNICATION_ERROR, "offline.status-error-unexpected-error",
489                     reason);
490         }
491     }
492
493     @Override
494     public ShellySettingsDevice getDeviceInfo() throws ShellyApiException {
495         Shelly2DeviceSettings device = callApi("/shelly", Shelly2DeviceSettings.class);
496         ShellySettingsDevice info = new ShellySettingsDevice();
497         info.hostname = getString(device.id);
498         info.fw = getString(device.firmware);
499         info.type = getString(device.model);
500         info.mac = getString(device.mac);
501         info.auth = getBool(device.authEnable);
502         info.gen = getInteger(device.gen);
503         return info;
504     }
505
506     @Override
507     public ShellySettingsStatus getStatus() throws ShellyApiException {
508         ShellyDeviceProfile profile = getProfile();
509         ShellySettingsStatus status = profile.status;
510         Shelly2DeviceStatusResult ds = apiRequest(SHELLYRPC_METHOD_GETSTATUS, null, Shelly2DeviceStatusResult.class);
511         status.time = ds.sys.time;
512         status.uptime = ds.sys.uptime;
513         status.cloud.connected = getBool(ds.cloud.connected);
514         status.mqtt.connected = getBool(ds.mqtt.connected);
515         status.wifiSta.ssid = getString(ds.wifi.ssid);
516         status.wifiSta.enabled = !status.wifiSta.ssid.isEmpty();
517         status.wifiSta.ip = getString(ds.wifi.staIP);
518         status.wifiSta.rssi = getInteger(ds.wifi.rssi);
519         status.fsFree = ds.sys.fsFree;
520         status.fsSize = ds.sys.fsSize;
521         status.discoverable = getBool(profile.settings.discoverable);
522
523         if (ds.sys.wakeupPeriod != null) {
524             profile.settings.sleepMode.period = ds.sys.wakeupPeriod / 60;
525         }
526
527         status.hasUpdate = status.update.hasUpdate = false;
528         status.update.oldVersion = getProfile().fwVersion;
529         if (ds.sys.availableUpdates != null) {
530             status.update.hasUpdate = ds.sys.availableUpdates.stable != null;
531             if (ds.sys.availableUpdates.stable != null) {
532                 status.update.newVersion = "v" + getString(ds.sys.availableUpdates.stable.version);
533             }
534             if (ds.sys.availableUpdates.beta != null) {
535                 status.update.betaVersion = "v" + getString(ds.sys.availableUpdates.beta.version);
536             }
537         }
538
539         if (ds.sys.wakeUpReason != null && ds.sys.wakeUpReason.boot != null) {
540             List<Object> values = new ArrayList<>();
541             String boot = getString(ds.sys.wakeUpReason.boot);
542             String cause = getString(ds.sys.wakeUpReason.cause);
543
544             // Index 0 is aggregated status, 1 boot, 2 cause
545             String reason = boot.equals(SHELLY2_WAKEUPO_BOOT_RESTART) ? ALARM_TYPE_RESTARTED : cause;
546             values.add(reason);
547             values.add(ds.sys.wakeUpReason.boot);
548             values.add(ds.sys.wakeUpReason.cause);
549             getThing().updateWakeupReason(values);
550         }
551
552         fillDeviceStatus(status, ds, false);
553         return status;
554     }
555
556     @Override
557     public void setSleepTime(int value) throws ShellyApiException {
558     }
559
560     @Override
561     public ShellyStatusRelay getRelayStatus(int relayIndex) throws ShellyApiException {
562         if (getProfile().status.wifiSta.ssid == null) {
563             // Update status when not yet initialized
564             getStatus();
565         }
566         return relayStatus;
567     }
568
569     @Override
570     public void setRelayTurn(int id, String turnMode) throws ShellyApiException {
571         Shelly2RpcRequestParams params = new Shelly2RpcRequestParams();
572         params.id = id;
573         params.on = SHELLY_API_ON.equals(turnMode);
574         apiRequest(SHELLYRPC_METHOD_SWITCH_SET, params, String.class);
575     }
576
577     @Override
578     public ShellyRollerStatus getRollerStatus(int rollerIndex) throws ShellyApiException {
579         if (rollerIndex < rollerStatus.size()) {
580             return rollerStatus.get(rollerIndex);
581         }
582         throw new IllegalArgumentException("Invalid rollerIndex on getRollerStatus");
583     }
584
585     @Override
586     public void setRollerTurn(int relayIndex, String turnMode) throws ShellyApiException {
587         String operation = "";
588         switch (turnMode) {
589             case SHELLY_ALWD_ROLLER_TURN_OPEN:
590                 operation = SHELLY2_COVER_CMD_OPEN;
591                 break;
592             case SHELLY_ALWD_ROLLER_TURN_CLOSE:
593                 operation = SHELLY2_COVER_CMD_CLOSE;
594                 break;
595             case SHELLY_ALWD_ROLLER_TURN_STOP:
596                 operation = SHELLY2_COVER_CMD_STOP;
597                 break;
598         }
599
600         apiRequest(new Shelly2RpcRequest().withMethod("Cover." + operation).withId(relayIndex));
601     }
602
603     @Override
604     public void setRollerPos(int relayIndex, int position) throws ShellyApiException {
605         apiRequest(
606                 new Shelly2RpcRequest().withMethod(SHELLYRPC_METHOD_COVER_SETPOS).withId(relayIndex).withPos(position));
607     }
608
609     @Override
610     public ShellyStatusSensor getSensorStatus() throws ShellyApiException {
611         return sensorData;
612     }
613
614     @Override
615     public void setAutoTimer(int index, String timerName, double value) throws ShellyApiException {
616         Shelly2RpcRequest req = new Shelly2RpcRequest().withMethod(SHELLYRPC_METHOD_SWITCH_SETCONFIG).withId(index);
617
618         req.params.withConfig();
619         req.params.config.name = "Switch" + index;
620         if (timerName.equals(SHELLY_TIMER_AUTOON)) {
621             req.params.config.autoOn = value > 0;
622             req.params.config.autoOnDelay = value;
623         } else {
624             req.params.config.autoOff = value > 0;
625             req.params.config.autoOffDelay = value;
626         }
627         apiRequest(req);
628     }
629
630     @Override
631     public ShellySettingsLogin getLoginSettings() throws ShellyApiException {
632         return new ShellySettingsLogin();
633     }
634
635     @Override
636     public ShellySettingsLogin setLoginCredentials(String user, String password) throws ShellyApiException {
637         Shelly2RpcRequestParams params = new Shelly2RpcRequestParams();
638         params.user = "admin";
639         params.realm = config.serviceName;
640         params.ha1 = sha256(params.user + ":" + params.realm + ":" + password);
641         apiRequest(SHELLYRPC_METHOD_AUTHSET, params, String.class);
642
643         ShellySettingsLogin res = new ShellySettingsLogin();
644         res.enabled = true;
645         res.username = params.user;
646         res.password = password;
647         return new ShellySettingsLogin();
648     }
649
650     @Override
651     public boolean setWiFiRangeExtender(boolean enable) throws ShellyApiException {
652         Shelly2RpcRequestParams params = new Shelly2RpcRequestParams().withConfig();
653         params.config.ap = new Shelly2DeviceConfigAp();
654         params.config.ap.rangeExtender = new Shelly2DeviceConfigApRE();
655         params.config.ap.rangeExtender.enable = enable;
656         Shelly2WsConfigResult res = apiRequest(SHELLYRPC_METHOD_WIFISETCONG, params, Shelly2WsConfigResult.class);
657         return res.restartRequired;
658     }
659
660     @Override
661     public boolean setEthernet(boolean enable) throws ShellyApiException {
662         Shelly2RpcRequestParams params = new Shelly2RpcRequestParams().withConfig();
663         params.config.enable = enable;
664         Shelly2WsConfigResult res = apiRequest(SHELLYRPC_METHOD_ETHSETCONG, params, Shelly2WsConfigResult.class);
665         return res.restartRequired;
666     }
667
668     @Override
669     public boolean setBluetooth(boolean enable) throws ShellyApiException {
670         Shelly2RpcRequestParams params = new Shelly2RpcRequestParams().withConfig();
671         params.config.enable = enable;
672         Shelly2WsConfigResult res = apiRequest(SHELLYRPC_METHOD_BLESETCONG, params, Shelly2WsConfigResult.class);
673         return res.restartRequired;
674     }
675
676     @Override
677     public String deviceReboot() throws ShellyApiException {
678         return apiRequest(SHELLYRPC_METHOD_REBOOT, null, String.class);
679     }
680
681     @Override
682     public String factoryReset() throws ShellyApiException {
683         return apiRequest(SHELLYRPC_METHOD_RESET, null, String.class);
684     }
685
686     @Override
687     public ShellyOtaCheckResult checkForUpdate() throws ShellyApiException {
688         Shelly2DeviceStatusSysAvlUpdate status = apiRequest(SHELLYRPC_METHOD_CHECKUPD, null,
689                 Shelly2DeviceStatusSysAvlUpdate.class);
690         ShellyOtaCheckResult result = new ShellyOtaCheckResult();
691         result.status = status.stable != null || status.beta != null ? "new" : "ok";
692         return result;
693     }
694
695     @Override
696     public ShellySettingsUpdate firmwareUpdate(String fwurl) throws ShellyApiException {
697         ShellySettingsUpdate res = new ShellySettingsUpdate();
698         boolean prod = fwurl.contains("update");
699         boolean beta = fwurl.contains("beta");
700
701         Shelly2RpcRequestParams params = new Shelly2RpcRequestParams();
702         if (prod || beta) {
703             params.stage = prod || beta ? "stable" : "beta";
704         } else {
705             params.url = fwurl;
706         }
707         apiRequest(SHELLYRPC_METHOD_UPDATE, params, String.class);
708         res.status = "Update initiated";
709         return res;
710     }
711
712     @Override
713     public String setCloud(boolean enable) throws ShellyApiException {
714         Shelly2RpcRequestParams params = new Shelly2RpcRequestParams().withConfig();
715         params.config.enable = enable;
716         Shelly2WsConfigResult res = apiRequest(SHELLYRPC_METHOD_CLOUDSET, params, Shelly2WsConfigResult.class);
717         return res.restartRequired ? "restart required" : "ok";
718     }
719
720     @Override
721     public String setDebug(boolean enabled) throws ShellyApiException {
722         return "failed";
723     }
724
725     @Override
726     public String getDebugLog(String id) throws ShellyApiException {
727         return ""; // Gen2 uses WS to publish debug log
728     }
729
730     /*
731      * The following API calls are not yet relevant, because currently there a no Plus/Pro (Gen2) devices of those
732      * categories (e.g. bulbs)
733      */
734     @Override
735     public void setLedStatus(String ledName, Boolean value) throws ShellyApiException {
736         throw new ShellyApiException("API call not implemented");
737     }
738
739     @Override
740     public ShellyStatusLight getLightStatus() throws ShellyApiException {
741         throw new ShellyApiException("API call not implemented");
742     }
743
744     @Override
745     public ShellyShortLightStatus getLightStatus(int index) throws ShellyApiException {
746         throw new ShellyApiException("API call not implemented");
747     }
748
749     @Override
750     public void setLightParm(int lightIndex, String parm, String value) throws ShellyApiException {
751         throw new ShellyApiException("API call not implemented");
752     }
753
754     @Override
755     public void setLightParms(int lightIndex, Map<String, String> parameters) throws ShellyApiException {
756         throw new ShellyApiException("API call not implemented");
757     }
758
759     @Override
760     public ShellyShortLightStatus setLightTurn(int id, String turnMode) throws ShellyApiException {
761         throw new ShellyApiException("API call not implemented");
762     }
763
764     @Override
765     public void setBrightness(int id, int brightness, boolean autoOn) throws ShellyApiException {
766         throw new ShellyApiException("API call not implemented");
767     }
768
769     @Override
770     public void setLightMode(String mode) throws ShellyApiException {
771         throw new ShellyApiException("API call not implemented");
772     }
773
774     @Override
775     public void setValveMode(int valveId, boolean auto) throws ShellyApiException {
776         throw new ShellyApiException("API call not implemented");
777     }
778
779     @Override
780     public void setValvePosition(int valveId, double value) throws ShellyApiException {
781         throw new ShellyApiException("API call not implemented");
782     }
783
784     @Override
785     public void setValveTemperature(int valveId, int value) throws ShellyApiException {
786         throw new ShellyApiException("API call not implemented");
787     }
788
789     @Override
790     public void setValveProfile(int valveId, int value) throws ShellyApiException {
791         throw new ShellyApiException("API call not implemented");
792     }
793
794     @Override
795     public void setValveBoostTime(int valveId, int value) throws ShellyApiException {
796         throw new ShellyApiException("API call not implemented");
797     }
798
799     @Override
800     public void startValveBoost(int valveId, int value) throws ShellyApiException {
801         throw new ShellyApiException("API call not implemented");
802     }
803
804     @Override
805     public String resetStaCache() throws ShellyApiException {
806         throw new ShellyApiException("API call not implemented");
807     }
808
809     @Override
810     public void setActionURLs() throws ShellyApiException {
811         // not relevant for Gen2
812     }
813
814     @Override
815     public ShellySettingsLogin setCoIoTPeer(String peer) throws ShellyApiException {
816         // not relevant for Gen2
817         return new ShellySettingsLogin();
818     }
819
820     @Override
821     public String getCoIoTDescription() {
822         return ""; // not relevant to Gen2
823     }
824
825     @Override
826     public void sendIRKey(String keyCode) throws ShellyApiException, IllegalArgumentException {
827         throw new ShellyApiException("API call not implemented");
828     }
829
830     @Override
831     public String setWiFiRecovery(boolean enable) throws ShellyApiException {
832         return "failed"; // not supported by Gen2
833     }
834
835     @Override
836     public String setApRoaming(boolean enable) throws ShellyApiException {
837         return "false";// not supported by Gen2
838     }
839
840     private void asyncApiRequest(String method) throws ShellyApiException {
841         Shelly2RpcBaseMessage request = buildRequest(method, null);
842         reconnect();
843         rpcSocket.sendMessage(gson.toJson(request)); // submit, result wull be async
844     }
845
846     public <T> T apiRequest(String method, @Nullable Object params, Class<T> classOfT) throws ShellyApiException {
847         String json = "";
848         Shelly2RpcBaseMessage req = buildRequest(method, params);
849         try {
850             reconnect(); // make sure WS is connected
851
852             if (authInfo.realm != null) {
853                 req.auth = buildAuthRequest(authInfo, config.userId, config.serviceName, config.password);
854             }
855             json = rpcPost(gson.toJson(req));
856         } catch (ShellyApiException e) {
857             ShellyApiResult res = e.getApiResult();
858             String auth = getString(res.authResponse);
859             if (res.isHttpAccessUnauthorized() && !auth.isEmpty()) {
860                 String[] options = auth.split(",");
861                 for (String o : options) {
862                     String key = substringBefore(o, "=").stripLeading().trim();
863                     String value = substringAfter(o, "=").replaceAll("\"", "").trim();
864                     switch (key) {
865                         case "Digest qop":
866                             break;
867                         case "realm":
868                             authInfo.realm = value;
869                             break;
870                         case "nonce":
871                             authInfo.nonce = Long.parseLong(value, 16);
872                             break;
873                         case "algorithm":
874                             authInfo.algorithm = value;
875                             break;
876                     }
877                 }
878                 authInfo.nc = 1;
879                 req.auth = buildAuthRequest(authInfo, config.userId, authInfo.realm, config.password);
880                 json = rpcPost(gson.toJson(req));
881             } else {
882                 throw e;
883             }
884         }
885         json = gson.toJson(gson.fromJson(json, Shelly2RpcBaseMessage.class).result);
886         return fromJson(gson, json, classOfT);
887     }
888
889     public <T> T apiRequest(Shelly2RpcRequest request, Class<T> classOfT) throws ShellyApiException {
890         return apiRequest(request.method, request.params, classOfT);
891     }
892
893     public String apiRequest(Shelly2RpcRequest request) throws ShellyApiException {
894         return apiRequest(request.method, request.params, String.class);
895     }
896
897     private String rpcPost(String postData) throws ShellyApiException {
898         return httpPost("/rpc", postData);
899     }
900
901     private void reconnect() throws ShellyApiException {
902         if (!rpcSocket.isConnected()) {
903             logger.debug("{}: Connect Rpc Socket (discovery = {})", thingName, discovery);
904             rpcSocket.connect();
905         }
906     }
907
908     private void disconnect() {
909         if (rpcSocket.isConnected()) {
910             rpcSocket.disconnect();
911         }
912     }
913
914     public Shelly2RpctInterface getRpcHandler() {
915         return this;
916     }
917
918     @Override
919     public void close() {
920         logger.debug("{}: Closing Rpc API (socket is {}, discovery={})", thingName,
921                 rpcSocket.isConnected() ? "connected" : "disconnected", discovery);
922         disconnect();
923         initialized = false;
924     }
925
926     private void incProtErrors() {
927         if (thing != null) {
928             thing.incProtErrors();
929         }
930     }
931 }