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