2 * Copyright (c) 2010-2023 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.shelly.internal.api2;
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.*;
20 import java.io.BufferedReader;
21 import java.io.IOException;
22 import java.io.InputStream;
23 import java.io.InputStreamReader;
24 import java.io.UncheckedIOException;
25 import java.util.ArrayList;
26 import java.util.List;
28 import java.util.stream.Collectors;
30 import org.eclipse.jdt.annotation.NonNullByDefault;
31 import org.eclipse.jdt.annotation.Nullable;
32 import org.eclipse.jetty.client.HttpClient;
33 import org.eclipse.jetty.http.HttpStatus;
34 import org.eclipse.jetty.websocket.api.StatusCode;
35 import org.openhab.binding.shelly.internal.api.ShellyApiException;
36 import org.openhab.binding.shelly.internal.api.ShellyApiInterface;
37 import org.openhab.binding.shelly.internal.api.ShellyApiResult;
38 import org.openhab.binding.shelly.internal.api.ShellyDeviceProfile;
39 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyInputState;
40 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyOtaCheckResult;
41 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyRollerStatus;
42 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySensorSleepMode;
43 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsDevice;
44 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsEMeter;
45 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsLogin;
46 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsMeter;
47 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsRelay;
48 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsStatus;
49 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsUpdate;
50 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsWiFiNetwork;
51 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyShortLightStatus;
52 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyShortStatusRelay;
53 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyStatusLight;
54 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyStatusRelay;
55 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyStatusSensor;
56 import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2AuthResponse;
57 import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2ConfigParms;
58 import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2DeviceConfig.Shelly2DeviceConfigSta;
59 import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2DeviceConfig.Shelly2GetConfigResult;
60 import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2DeviceConfigAp;
61 import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2DeviceConfigAp.Shelly2DeviceConfigApRE;
62 import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2DeviceSettings;
63 import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2DeviceStatus.Shelly2DeviceStatusResult;
64 import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2DeviceStatus.Shelly2DeviceStatusSys.Shelly2DeviceStatusSysAvlUpdate;
65 import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2NotifyEvent;
66 import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2RpcBaseMessage;
67 import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2RpcNotifyEvent;
68 import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2RpcNotifyStatus;
69 import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2RpcNotifyStatus.Shelly2NotifyStatus;
70 import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2RpcRequest;
71 import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2RpcRequest.Shelly2RpcRequestParams;
72 import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2WsConfigResponse;
73 import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2WsConfigResponse.Shelly2WsConfigResult;
74 import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.ShellyScriptListResponse;
75 import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.ShellyScriptListResponse.ShellyScriptListEntry;
76 import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.ShellyScriptPutCodeParams;
77 import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.ShellyScriptResponse;
78 import org.openhab.binding.shelly.internal.config.ShellyThingConfiguration;
79 import org.openhab.binding.shelly.internal.handler.ShellyThingInterface;
80 import org.openhab.binding.shelly.internal.handler.ShellyThingTable;
81 import org.openhab.core.library.unit.SIUnits;
82 import org.openhab.core.thing.ThingStatusDetail;
83 import org.slf4j.Logger;
84 import org.slf4j.LoggerFactory;
87 * {@link Shelly2ApiRpc} implements Gen2 RPC interface
89 * @author Markus Michels - Initial contribution
92 public class Shelly2ApiRpc extends Shelly2ApiClient implements ShellyApiInterface, Shelly2RpctInterface {
93 private final Logger logger = LoggerFactory.getLogger(Shelly2ApiRpc.class);
94 private final @Nullable ShellyThingTable thingTable;
96 protected boolean initialized = false;
97 private boolean discovery = false;
98 private Shelly2RpcSocket rpcSocket = new Shelly2RpcSocket();
99 private Shelly2AuthResponse authInfo = new Shelly2AuthResponse();
102 * Regular constructor - called by Thing handler
104 * @param thingName Symbolic thing name
105 * @param thing Thing Handler (ThingHandlerInterface)
107 public Shelly2ApiRpc(String thingName, ShellyThingTable thingTable, ShellyThingInterface thing) {
108 super(thingName, thing);
109 this.thingName = thingName;
111 this.thingTable = thingTable;
113 getProfile().initFromThingType(thing.getThingType());
114 } catch (ShellyApiException e) {
115 logger.info("{}: Shelly2 API initialization failed!", thingName, e);
120 * Simple initialization - called by discovery handler
122 * @param thingName Symbolic thing name
123 * @param config Thing Configuration
124 * @param httpClient HTTP Client to be passed to ShellyHttpClient
126 public Shelly2ApiRpc(String thingName, ShellyThingConfiguration config, HttpClient httpClient) {
127 super(thingName, config, httpClient);
128 this.thingName = thingName;
129 this.thingTable = null;
130 this.discovery = true;
134 public void initialize() throws ShellyApiException {
136 rpcSocket = new Shelly2RpcSocket(thingName, thingTable, config.deviceIp);
137 rpcSocket.addMessageHandler(this);
140 if (rpcSocket.isConnected()) {
141 logger.debug("{}: Disconnect Rpc Socket on initialize", thingName);
148 public boolean isInitialized() {
153 public void startScan() {
154 if (config.enableBluGateway) {
156 installScript(SHELLY2_BLU_GWSCRIPT);
157 } catch (ShellyApiException e) {
162 @SuppressWarnings("null")
164 public ShellyDeviceProfile getDeviceProfile(String thingType) throws ShellyApiException {
165 ShellyDeviceProfile profile = thing != null ? getProfile() : new ShellyDeviceProfile();
167 Shelly2GetConfigResult dc = apiRequest(SHELLYRPC_METHOD_GETCONFIG, null, Shelly2GetConfigResult.class);
168 profile.isGen2 = true;
169 profile.settingsJson = gson.toJson(dc);
170 profile.thingName = thingName;
171 profile.settings.name = profile.status.name = dc.sys.device.name;
172 profile.name = getString(profile.settings.name);
173 profile.settings.timezone = getString(dc.sys.location.tz);
174 profile.settings.discoverable = getBool(dc.sys.device.discoverable);
175 if (dc.wifi != null && dc.wifi.ap != null && dc.wifi.ap.rangeExtender != null) {
176 profile.settings.wifiAp.rangeExtender = getBool(dc.wifi.ap.rangeExtender.enable);
178 if (dc.cloud != null) {
179 profile.settings.cloud.enabled = getBool(dc.cloud.enable);
181 if (dc.mqtt != null) {
182 profile.settings.mqtt.enable = getBool(dc.mqtt.enable);
184 if (dc.sys.sntp != null) {
185 profile.settings.sntp.server = dc.sys.sntp.server;
188 profile.isRoller = dc.cover0 != null;
189 profile.settings.relays = fillRelaySettings(profile, dc);
190 profile.settings.inputs = fillInputSettings(profile, dc);
191 profile.settings.rollers = fillRollerSettings(profile, dc);
193 profile.isEMeter = true;
194 profile.numInputs = profile.settings.inputs != null ? profile.settings.inputs.size() : 0;
195 profile.numRelays = profile.settings.relays != null ? profile.settings.relays.size() : 0;
196 profile.numRollers = profile.settings.rollers != null ? profile.settings.rollers.size() : 0;
197 profile.hasRelays = profile.numRelays > 0 || profile.numRollers > 0;
199 if (profile.hasRelays) {
200 profile.mode = profile.isRoller ? SHELLY_CLASS_ROLLER : SHELLY_CLASS_RELAY;
203 ShellySettingsDevice device = getDeviceInfo();
204 profile.settings.device = device;
205 profile.hostname = device.hostname;
206 profile.deviceType = device.type;
207 profile.mac = device.mac;
208 profile.auth = device.auth;
209 if (config.serviceName.isEmpty()) {
210 config.serviceName = getString(profile.hostname);
212 profile.fwDate = substringBefore(device.fw, "/");
213 profile.fwVersion = substringBefore(ShellyDeviceProfile.extractFwVersion(device.fw.replace("/", "/v")), "-");
214 profile.status.update.oldVersion = profile.fwVersion;
215 profile.status.hasUpdate = profile.status.update.hasUpdate = false;
217 if (dc.eth != null) {
218 profile.settings.ethernet = getBool(dc.eth.enable);
220 if (dc.ble != null) {
221 profile.settings.bluetooth = getBool(dc.ble.enable);
224 profile.settings.wifiSta = new ShellySettingsWiFiNetwork();
225 profile.settings.wifiSta1 = new ShellySettingsWiFiNetwork();
226 fillWiFiSta(dc.wifi.sta, profile.settings.wifiSta);
227 fillWiFiSta(dc.wifi.sta1, profile.settings.wifiSta1);
229 profile.numMeters = 0;
230 if (profile.hasRelays) {
231 profile.status.relays = new ArrayList<>();
232 relayStatus.relays = new ArrayList<>();
233 profile.numMeters = profile.isRoller ? profile.numRollers : profile.numRelays;
234 for (int i = 0; i < profile.numRelays; i++) {
235 profile.status.relays.add(new ShellySettingsRelay());
236 relayStatus.relays.add(new ShellyShortStatusRelay());
240 if (profile.numInputs > 0) {
241 profile.status.inputs = new ArrayList<>();
242 relayStatus.inputs = new ArrayList<>();
243 for (int i = 0; i < profile.numInputs; i++) {
244 ShellyInputState input = new ShellyInputState();
247 input.eventCount = 0;
248 profile.status.inputs.add(input);
249 relayStatus.inputs.add(input);
253 if (dc.em0 != null) {
254 profile.numMeters = 3;
257 if (profile.numMeters > 0) {
258 profile.status.meters = new ArrayList<>();
259 profile.status.emeters = new ArrayList<>();
260 relayStatus.meters = new ArrayList<>();
262 for (int i = 0; i < profile.numMeters; i++) {
263 profile.status.meters.add(new ShellySettingsMeter());
264 profile.status.emeters.add(new ShellySettingsEMeter());
265 relayStatus.meters.add(new ShellySettingsMeter());
269 if (profile.isRoller) {
270 profile.status.rollers = new ArrayList<>();
271 for (int i = 0; i < profile.numRollers; i++) {
272 ShellyRollerStatus rs = new ShellyRollerStatus();
273 profile.status.rollers.add(rs);
274 rollerStatus.add(rs);
278 profile.status.dimmers = profile.isDimmer ? new ArrayList<>() : null;
279 profile.status.lights = profile.isBulb ? new ArrayList<>() : null;
280 profile.status.thermostats = profile.isTRV ? new ArrayList<>() : null;
282 if (profile.hasBattery) {
283 profile.settings.sleepMode = new ShellySensorSleepMode();
284 profile.settings.sleepMode.unit = "m";
285 profile.settings.sleepMode.period = dc.sys.sleep != null ? dc.sys.sleep.wakeupPeriod / 60 : 720;
286 checkSetWsCallback();
289 profile.initialized = true;
291 getStatus(); // make sure profile.status is initialized (e.g,. relay/meter status)
292 asyncApiRequest(SHELLYRPC_METHOD_GETSTATUS); // request periodic status updates from device
295 logger.debug("{}: BLU Gateway support enabled for this device: {}", thingName, config.enableBluGateway);
296 if (config.enableBluGateway) {
297 if (getBool(profile.settings.bluetooth)) {
298 installScript(SHELLY2_BLU_GWSCRIPT);
300 logger.debug("{}: Bluetooth needs to be enabled to activate BLU Gateway mode", thingName);
303 } catch (ShellyApiException e) {
304 logger.debug("{}: Device config failed", thingName, e);
311 private void fillWiFiSta(@Nullable Shelly2DeviceConfigSta from, ShellySettingsWiFiNetwork to) {
312 to.enabled = from != null && !getString(from.ssid).isEmpty();
316 to.mask = from.netmask;
317 to.dns = from.nameserver;
321 private void checkSetWsCallback() throws ShellyApiException {
322 Shelly2ConfigParms wsConfig = apiRequest(SHELLYRPC_METHOD_WSGETCONFIG, null, Shelly2ConfigParms.class);
323 String url = "ws://" + config.localIp + ":" + config.localPort + "/shelly/wsevent";
324 if (!config.localIp.isEmpty() && !getBool(wsConfig.enable)
325 || !url.equalsIgnoreCase(getString(wsConfig.server))) {
326 logger.debug("{}: A battery device was detected without correct callback, fix it", thingName);
327 wsConfig.enable = true;
328 wsConfig.server = url;
329 Shelly2RpcRequest request = new Shelly2RpcRequest();
331 request.method = SHELLYRPC_METHOD_WSSETCONFIG;
332 request.params.config = wsConfig;
333 Shelly2WsConfigResponse response = apiRequest(SHELLYRPC_METHOD_WSSETCONFIG, request.params,
334 Shelly2WsConfigResponse.class);
335 if (response.result != null && response.result.restartRequired) {
336 logger.info("{}: WebSocket callback was updated, device is restarting", thingName);
337 getThing().getApi().deviceReboot();
338 getThing().reinitializeThing();
343 protected void installScript(String script) throws ShellyApiException {
344 String json = apiRequest(new Shelly2RpcRequest().withMethod(SHELLYRPC_METHOD_SCRIPT_LIST));
345 ShellyScriptListResponse scriptList = gson.fromJson(json, ShellyScriptListResponse.class);
349 logger.debug("{}: Install or restart script {} on Shelly Device", thingName, script);
350 boolean running = false, upload = false;
351 if (scriptList != null) {
352 for (ShellyScriptListEntry s : scriptList.scripts) {
353 if (s.name.startsWith(script)) {
356 logger.debug("{}: Script {} is already installed, id={}", thingName, script, ourId);
361 // get script code from bundle resources
362 String file = BUNDLE_RESOURCE_SCRIPTS + "/" + script;
363 ClassLoader cl = Shelly2ApiRpc.class.getClassLoader();
365 try (InputStream inputStream = cl.getResourceAsStream(file)) {
366 if (inputStream != null) {
367 code = new BufferedReader(new InputStreamReader(inputStream)).lines()
368 .collect(Collectors.joining("\n"));
370 } catch (IOException | UncheckedIOException e) {
371 logger.debug("{}: Installation of script {} failed: Unable to read {} from bundle resources!",
372 thingName, script, file, e);
376 boolean restart = false;
378 // script not installed -> install it
382 // verify that the same code version is active (avoid unnesesary flash updates)
383 json = apiRequest(new Shelly2RpcRequest().withMethod(SHELLYRPC_METHOD_SCRIPT_GETCODE).withId(ourId));
384 ShellyScriptResponse rsp = gson.fromJson(json, ShellyScriptResponse.class);
385 if (!rsp.data.trim().equals(code.trim())) {
386 logger.debug("{}: A script version was found, update to newest one", thingName);
389 logger.debug("{}: Same script version was found, restart", thingName);
392 } catch (ShellyApiException e) {
393 logger.debug("{}: Unable to read current script code -> force update (deviced returned: {})", thingName,
399 if (restart || (running && upload)) {
400 json = apiRequest(new Shelly2RpcRequest().withMethod(SHELLYRPC_METHOD_SCRIPT_STOP).withId(ourId));
401 // first stop running script
404 if (upload && ourId != -1) {
405 // Delete existing script
406 logger.debug("{}: Delete existing script", thingName);
407 json = apiRequest(new Shelly2RpcRequest().withMethod(SHELLYRPC_METHOD_SCRIPT_DELETE).withId(ourId));
411 logger.debug("{}: Script will be installed...", thingName);
413 // Create new script, get id
414 json = apiRequest(new Shelly2RpcRequest().withMethod(SHELLYRPC_METHOD_SCRIPT_CREATE).withName(script));
415 ShellyScriptResponse rsp = gson.fromJson(json, ShellyScriptResponse.class);
418 logger.debug("{}: Script has been created, id={}", thingName, ourId);
424 // Put script code for generated id
425 ShellyScriptPutCodeParams parms = new ShellyScriptPutCodeParams();
427 parms.append = false;
428 int length = code.length(), processed = 0, chunk = 1;
430 int nextlen = Math.min(1024, length - processed);
431 parms.code = code.substring(processed, processed + nextlen);
432 logger.debug("{}: Uploading chunk {} of script (total {} chars, {} processed)", thingName, chunk,
434 apiRequest(SHELLYRPC_METHOD_SCRIPT_PUTCODE, parms, String.class);
435 processed += nextlen;
438 } while (processed < length);
441 Shelly2RpcRequestParams params = new Shelly2RpcRequestParams().withConfig();
442 params.config.enable = true;
443 apiRequest(SHELLYRPC_METHOD_SCRIPT_SETCONFIG, params, String.class);
447 // Script was created or is there and stopped -> start it
448 json = apiRequest(new Shelly2RpcRequest().withMethod(SHELLYRPC_METHOD_SCRIPT_START).withId(ourId));
449 logger.debug("{}: Script {} was {} successful", thingName, script,
450 restart ? "restarted" : "installed and started");
455 public void onConnect(String deviceIp, boolean connected) {
456 if (thing == null && thingTable != null) {
457 thing = thingTable.getThing(deviceIp);
458 logger.debug("{}: Get thing from thingTable", thingName);
463 public void onNotifyStatus(Shelly2RpcNotifyStatus message) {
464 logger.debug("{}: NotifyStatus update received: {}", thingName, gson.toJson(message));
466 ShellyThingInterface t = thing;
468 logger.debug("{}: No matching thing on NotifyStatus for {}, ignore (src={}, dst={}, discovery={})",
469 thingName, thingName, message.src, message.dst, discovery);
472 if (t.isStopping()) {
473 logger.debug("{}: Thing is shutting down, ignore WebSocket message", thingName);
476 if (!t.isThingOnline() && t.getThingStatusDetail() != ThingStatusDetail.CONFIGURATION_PENDING) {
477 logger.debug("{}: Thing is not in online state/connectable, ignore NotifyStatus", thingName);
481 getThing().incProtMessages();
482 if (message.error != null) {
483 if (message.error.code == HttpStatus.UNAUTHORIZED_401 && !getString(message.error.message).isEmpty()) {
484 // Save nonce for notification
485 Shelly2AuthResponse auth = gson.fromJson(message.error.message, Shelly2AuthResponse.class);
486 if (auth != null && auth.realm == null) {
487 logger.debug("{}: Authentication data received: {}", thingName, message.error.message);
491 logger.debug("{}: Error status received - {} {}", thingName, message.error.code,
492 message.error.message);
497 Shelly2NotifyStatus params = message.params;
498 if (params != null) {
499 if (getThing().getThingStatusDetail() != ThingStatusDetail.FIRMWARE_UPDATING) {
500 getThing().setThingOnline();
503 boolean updated = false;
504 ShellyDeviceProfile profile = getProfile();
505 ShellySettingsStatus status = profile.status;
506 if (params.sys != null) {
507 if (getBool(params.sys.restartRequired)) {
508 logger.warn("{}: Device requires restart to activate changes", thingName);
510 status.uptime = params.sys.uptime;
512 status.temperature = SHELLY_API_INVTEMP; // mark invalid
513 updated |= fillDeviceStatus(status, message.params, true);
514 if (getDouble(status.temperature) == SHELLY_API_INVTEMP) {
515 // no device temp available
516 status.temperature = null;
518 updated |= updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_ITEMP,
519 toQuantityType(getDouble(status.tmp.tC), DIGITS_NONE, SIUnits.CELSIUS));
522 profile.status = status;
524 getThing().restartWatchdog();
527 } catch (ShellyApiException e) {
528 logger.debug("{}: Unable to process status update", thingName, e);
534 public void onNotifyEvent(Shelly2RpcNotifyEvent message) {
536 logger.debug("{}: NotifyEvent received: {}", thingName, gson.toJson(message));
537 ShellyDeviceProfile profile = getProfile();
539 getThing().incProtMessages();
540 getThing().restartWatchdog();
542 for (Shelly2NotifyEvent e : message.params.events) {
544 case SHELLY2_EVENT_BTNUP:
545 case SHELLY2_EVENT_BTNDOWN:
546 String bgroup = getProfile().getInputGroup(e.id);
547 updateChannel(bgroup, CHANNEL_INPUT + profile.getInputSuffix(e.id),
548 getOnOff(SHELLY2_EVENT_BTNDOWN.equals(getString(e.event))));
549 getThing().triggerButton(profile.getInputGroup(e.id), e.id,
550 mapValue(MAP_INPUT_EVENT_ID, e.event));
553 case SHELLY2_EVENT_1PUSH:
554 case SHELLY2_EVENT_2PUSH:
555 case SHELLY2_EVENT_3PUSH:
556 case SHELLY2_EVENT_LPUSH:
557 case SHELLY2_EVENT_SLPUSH:
558 case SHELLY2_EVENT_LSPUSH:
559 if (e.id < profile.numInputs) {
560 ShellyInputState input = relayStatus.inputs.get(e.id);
561 input.event = getString(MAP_INPUT_EVENT_TYPE.get(e.event));
562 input.eventCount = getInteger(input.eventCount) + 1;
563 relayStatus.inputs.set(e.id, input);
564 profile.status.inputs.set(e.id, input);
566 String group = getProfile().getInputGroup(e.id);
567 updateChannel(group, CHANNEL_STATUS_EVENTTYPE + profile.getInputSuffix(e.id),
568 getStringType(input.event));
569 updateChannel(group, CHANNEL_STATUS_EVENTCOUNT + profile.getInputSuffix(e.id),
570 getDecimal(input.eventCount));
571 getThing().triggerButton(profile.getInputGroup(e.id), e.id,
572 mapValue(MAP_INPUT_EVENT_ID, e.event));
575 case SHELLY2_EVENT_CFGCHANGED:
576 logger.debug("{}: Configuration update detected, re-initialize", thingName);
577 getThing().requestUpdates(1, true); // refresh config
580 case SHELLY2_EVENT_OTASTART:
581 logger.debug("{}: Firmware update started: {}", thingName, getString(e.msg));
582 getThing().postEvent(e.event, true);
583 getThing().setThingOffline(ThingStatusDetail.FIRMWARE_UPDATING,
584 "offline.status-error-fwupgrade");
586 case SHELLY2_EVENT_OTAPROGRESS:
587 logger.debug("{}: Firmware update in progress: {}", thingName, getString(e.msg));
588 getThing().postEvent(e.event, false);
590 case SHELLY2_EVENT_OTADONE:
591 logger.debug("{}: Firmware update completed: {}", thingName, getString(e.msg));
592 getThing().setThingOffline(ThingStatusDetail.CONFIGURATION_PENDING,
593 "offline.status-error-restarted");
594 getThing().requestUpdates(1, true); // refresh config
596 case SHELLY2_EVENT_SLEEP:
597 logger.debug("{}: Device went to sleep mode", thingName);
599 case SHELLY2_EVENT_WIFICONNFAILED:
600 logger.debug("{}: WiFi connect failed, check setup, reason {}", thingName,
601 getInteger(e.reason));
602 getThing().postEvent(e.event, false);
604 case SHELLY2_EVENT_WIFIDISCONNECTED:
605 logger.debug("{}: WiFi disconnected, reason {}", thingName, getInteger(e.reason));
606 getThing().postEvent(e.event, false);
609 logger.debug("{}: Event {} was not handled", thingName, e.event);
612 } catch (ShellyApiException e) {
613 logger.debug("{}: Unable to process event", thingName, e);
619 public void onMessage(String message) {
620 logger.debug("{}: Unexpected RPC message received: {}", thingName, message);
625 public void onClose(int statusCode, String reason) {
627 logger.debug("{}: WebSocket connection closed, status = {}/{}", thingName, statusCode, getString(reason));
628 if (statusCode == StatusCode.ABNORMAL && !discovery && getProfile().alwaysOn) { // e.g. device rebooted
629 thingOffline("WebSocket connection closed abnormal");
631 } catch (ShellyApiException e) {
632 logger.debug("{}: Exception on onClose()", thingName, e);
638 public void onError(Throwable cause) {
639 logger.debug("{}: WebSocket error", thingName);
640 if (thing != null && thing.getProfile().alwaysOn) {
641 thingOffline("WebSocket error");
645 private void thingOffline(String reason) {
646 if (thing != null) { // do not reinit of battery powered devices with sleep mode
647 thing.setThingOffline(ThingStatusDetail.COMMUNICATION_ERROR, "offline.status-error-unexpected-error",
653 public ShellySettingsDevice getDeviceInfo() throws ShellyApiException {
654 Shelly2DeviceSettings device = callApi("/shelly", Shelly2DeviceSettings.class);
655 ShellySettingsDevice info = new ShellySettingsDevice();
656 info.hostname = getString(device.id);
657 info.fw = getString(device.firmware);
658 info.type = getString(device.model);
659 info.mac = getString(device.mac);
660 info.auth = getBool(device.authEnable);
661 info.gen = getInteger(device.gen);
666 public ShellySettingsStatus getStatus() throws ShellyApiException {
667 ShellyDeviceProfile profile = getProfile();
668 ShellySettingsStatus status = profile.status;
669 Shelly2DeviceStatusResult ds = apiRequest(SHELLYRPC_METHOD_GETSTATUS, null, Shelly2DeviceStatusResult.class);
670 status.time = ds.sys.time;
671 status.uptime = ds.sys.uptime;
672 status.cloud.connected = getBool(ds.cloud.connected);
673 status.mqtt.connected = getBool(ds.mqtt.connected);
674 status.wifiSta.ssid = getString(ds.wifi.ssid);
675 status.wifiSta.enabled = !status.wifiSta.ssid.isEmpty();
676 status.wifiSta.ip = getString(ds.wifi.staIP);
677 status.wifiSta.rssi = getInteger(ds.wifi.rssi);
678 status.fsFree = ds.sys.fsFree;
679 status.fsSize = ds.sys.fsSize;
680 status.discoverable = getBool(profile.settings.discoverable);
682 if (ds.sys.wakeupPeriod != null) {
683 profile.settings.sleepMode.period = ds.sys.wakeupPeriod / 60;
686 status.hasUpdate = status.update.hasUpdate = false;
687 status.update.oldVersion = getProfile().fwVersion;
688 if (ds.sys.availableUpdates != null) {
689 status.update.hasUpdate = ds.sys.availableUpdates.stable != null;
690 if (ds.sys.availableUpdates.stable != null) {
691 status.update.newVersion = "v" + getString(ds.sys.availableUpdates.stable.version);
693 if (ds.sys.availableUpdates.beta != null) {
694 status.update.betaVersion = "v" + getString(ds.sys.availableUpdates.beta.version);
698 if (ds.sys.wakeUpReason != null && ds.sys.wakeUpReason.boot != null) {
699 List<Object> values = new ArrayList<>();
700 String boot = getString(ds.sys.wakeUpReason.boot);
701 String cause = getString(ds.sys.wakeUpReason.cause);
703 // Index 0 is aggregated status, 1 boot, 2 cause
704 String reason = boot.equals(SHELLY2_WAKEUPO_BOOT_RESTART) ? ALARM_TYPE_RESTARTED : cause;
706 values.add(ds.sys.wakeUpReason.boot);
707 values.add(ds.sys.wakeUpReason.cause);
708 getThing().updateWakeupReason(values);
711 fillDeviceStatus(status, ds, false);
716 public void setSleepTime(int value) throws ShellyApiException {
720 public ShellyStatusRelay getRelayStatus(int relayIndex) throws ShellyApiException {
721 if (getProfile().status.wifiSta.ssid == null) {
722 // Update status when not yet initialized
729 public void setRelayTurn(int id, String turnMode) throws ShellyApiException {
730 Shelly2RpcRequestParams params = new Shelly2RpcRequestParams();
732 params.on = SHELLY_API_ON.equals(turnMode);
733 apiRequest(SHELLYRPC_METHOD_SWITCH_SET, params, String.class);
737 public ShellyRollerStatus getRollerStatus(int rollerIndex) throws ShellyApiException {
738 if (rollerIndex < rollerStatus.size()) {
739 return rollerStatus.get(rollerIndex);
741 throw new IllegalArgumentException("Invalid rollerIndex on getRollerStatus");
745 public void setRollerTurn(int relayIndex, String turnMode) throws ShellyApiException {
746 String operation = "";
748 case SHELLY_ALWD_ROLLER_TURN_OPEN:
749 operation = SHELLY2_COVER_CMD_OPEN;
751 case SHELLY_ALWD_ROLLER_TURN_CLOSE:
752 operation = SHELLY2_COVER_CMD_CLOSE;
754 case SHELLY_ALWD_ROLLER_TURN_STOP:
755 operation = SHELLY2_COVER_CMD_STOP;
759 apiRequest(new Shelly2RpcRequest().withMethod("Cover." + operation).withId(relayIndex));
763 public void setRollerPos(int relayIndex, int position) throws ShellyApiException {
765 new Shelly2RpcRequest().withMethod(SHELLYRPC_METHOD_COVER_SETPOS).withId(relayIndex).withPos(position));
769 public ShellyStatusSensor getSensorStatus() throws ShellyApiException {
774 public void setAutoTimer(int index, String timerName, double value) throws ShellyApiException {
775 Shelly2RpcRequest req = new Shelly2RpcRequest().withMethod(SHELLYRPC_METHOD_SWITCH_SETCONFIG).withId(index);
777 req.params.withConfig();
778 req.params.config.name = "Switch" + index;
779 if (timerName.equals(SHELLY_TIMER_AUTOON)) {
780 req.params.config.autoOn = value > 0;
781 req.params.config.autoOnDelay = value;
783 req.params.config.autoOff = value > 0;
784 req.params.config.autoOffDelay = value;
790 public void resetMeterTotal(int id) throws ShellyApiException {
794 public void muteSmokeAlarm(int index) throws ShellyApiException {
795 apiRequest(new Shelly2RpcRequest().withMethod(SHELLYRPC_METHOD_SMOKE_MUTE).withId(index));
799 public ShellySettingsLogin getLoginSettings() throws ShellyApiException {
800 return new ShellySettingsLogin();
804 public ShellySettingsLogin setLoginCredentials(String user, String password) throws ShellyApiException {
805 Shelly2RpcRequestParams params = new Shelly2RpcRequestParams();
806 params.user = "admin";
807 params.realm = config.serviceName;
808 params.ha1 = sha256(params.user + ":" + params.realm + ":" + password);
809 apiRequest(SHELLYRPC_METHOD_AUTHSET, params, String.class);
811 ShellySettingsLogin res = new ShellySettingsLogin();
813 res.username = params.user;
814 res.password = password;
815 return new ShellySettingsLogin();
819 public boolean setWiFiRangeExtender(boolean enable) throws ShellyApiException {
820 Shelly2RpcRequestParams params = new Shelly2RpcRequestParams().withConfig();
821 params.config.ap = new Shelly2DeviceConfigAp();
822 params.config.ap.rangeExtender = new Shelly2DeviceConfigApRE();
823 params.config.ap.rangeExtender.enable = enable;
824 Shelly2WsConfigResult res = apiRequest(SHELLYRPC_METHOD_WIFISETCONG, params, Shelly2WsConfigResult.class);
825 return res.restartRequired;
829 public boolean setEthernet(boolean enable) throws ShellyApiException {
830 Shelly2RpcRequestParams params = new Shelly2RpcRequestParams().withConfig();
831 params.config.enable = enable;
832 Shelly2WsConfigResult res = apiRequest(SHELLYRPC_METHOD_ETHSETCONG, params, Shelly2WsConfigResult.class);
833 return res.restartRequired;
837 public boolean setBluetooth(boolean enable) throws ShellyApiException {
838 Shelly2RpcRequestParams params = new Shelly2RpcRequestParams().withConfig();
839 params.config.enable = enable;
840 Shelly2WsConfigResult res = apiRequest(SHELLYRPC_METHOD_BLESETCONG, params, Shelly2WsConfigResult.class);
841 return res.restartRequired;
845 public String deviceReboot() throws ShellyApiException {
846 return apiRequest(SHELLYRPC_METHOD_REBOOT, null, String.class);
850 public String factoryReset() throws ShellyApiException {
851 return apiRequest(SHELLYRPC_METHOD_RESET, null, String.class);
855 public ShellyOtaCheckResult checkForUpdate() throws ShellyApiException {
856 Shelly2DeviceStatusSysAvlUpdate status = apiRequest(SHELLYRPC_METHOD_CHECKUPD, null,
857 Shelly2DeviceStatusSysAvlUpdate.class);
858 ShellyOtaCheckResult result = new ShellyOtaCheckResult();
859 result.status = status.stable != null || status.beta != null ? "new" : "ok";
864 public ShellySettingsUpdate firmwareUpdate(String fwurl) throws ShellyApiException {
865 ShellySettingsUpdate res = new ShellySettingsUpdate();
866 boolean prod = fwurl.contains("update");
867 boolean beta = fwurl.contains("beta");
869 Shelly2RpcRequestParams params = new Shelly2RpcRequestParams();
871 params.stage = prod || beta ? "stable" : "beta";
875 apiRequest(SHELLYRPC_METHOD_UPDATE, params, String.class);
876 res.status = "Update initiated";
881 public String setCloud(boolean enable) throws ShellyApiException {
882 Shelly2RpcRequestParams params = new Shelly2RpcRequestParams().withConfig();
883 params.config.enable = enable;
884 Shelly2WsConfigResult res = apiRequest(SHELLYRPC_METHOD_CLOUDSET, params, Shelly2WsConfigResult.class);
885 return res.restartRequired ? "restart required" : "ok";
889 public String setDebug(boolean enabled) throws ShellyApiException {
894 public String getDebugLog(String id) throws ShellyApiException {
895 return ""; // Gen2 uses WS to publish debug log
899 * The following API calls are not yet relevant, because currently there a no Plus/Pro (Gen2) devices of those
900 * categories (e.g. bulbs)
903 public void setLedStatus(String ledName, boolean value) throws ShellyApiException {
904 throw new ShellyApiException("API call not implemented");
908 public ShellyStatusLight getLightStatus() throws ShellyApiException {
909 throw new ShellyApiException("API call not implemented");
913 public ShellyShortLightStatus getLightStatus(int index) throws ShellyApiException {
914 throw new ShellyApiException("API call not implemented");
918 public void setLightParm(int lightIndex, String parm, String value) throws ShellyApiException {
919 throw new ShellyApiException("API call not implemented");
923 public void setLightParms(int lightIndex, Map<String, String> parameters) throws ShellyApiException {
924 throw new ShellyApiException("API call not implemented");
928 public ShellyShortLightStatus setLightTurn(int id, String turnMode) throws ShellyApiException {
929 throw new ShellyApiException("API call not implemented");
933 public void setBrightness(int id, int brightness, boolean autoOn) throws ShellyApiException {
934 throw new ShellyApiException("API call not implemented");
938 public void setLightMode(String mode) throws ShellyApiException {
939 throw new ShellyApiException("API call not implemented");
943 public void setValveMode(int valveId, boolean auto) throws ShellyApiException {
944 throw new ShellyApiException("API call not implemented");
948 public void setValvePosition(int valveId, double value) throws ShellyApiException {
949 throw new ShellyApiException("API call not implemented");
953 public void setValveTemperature(int valveId, int value) throws ShellyApiException {
954 throw new ShellyApiException("API call not implemented");
958 public void setValveProfile(int valveId, int value) throws ShellyApiException {
959 throw new ShellyApiException("API call not implemented");
963 public void setValveBoostTime(int valveId, int value) throws ShellyApiException {
964 throw new ShellyApiException("API call not implemented");
968 public void startValveBoost(int valveId, int value) throws ShellyApiException {
969 throw new ShellyApiException("API call not implemented");
973 public String resetStaCache() throws ShellyApiException {
974 throw new ShellyApiException("API call not implemented");
978 public void setActionURLs() throws ShellyApiException {
979 // not relevant for Gen2
983 public ShellySettingsLogin setCoIoTPeer(String peer) throws ShellyApiException {
984 // not relevant for Gen2
985 return new ShellySettingsLogin();
989 public String getCoIoTDescription() {
990 return ""; // not relevant to Gen2
994 public void sendIRKey(String keyCode) throws ShellyApiException, IllegalArgumentException {
995 throw new ShellyApiException("API call not implemented");
999 public String setWiFiRecovery(boolean enable) throws ShellyApiException {
1000 return "failed"; // not supported by Gen2
1004 public String setApRoaming(boolean enable) throws ShellyApiException {
1005 return "false";// not supported by Gen2
1008 private void asyncApiRequest(String method) throws ShellyApiException {
1009 Shelly2RpcBaseMessage request = buildRequest(method, null);
1011 rpcSocket.sendMessage(gson.toJson(request)); // submit, result wull be async
1014 @SuppressWarnings("null")
1015 public <T> T apiRequest(String method, @Nullable Object params, Class<T> classOfT) throws ShellyApiException {
1017 Shelly2RpcBaseMessage req = buildRequest(method, params);
1019 reconnect(); // make sure WS is connected
1021 if (authInfo.realm != null) {
1022 req.auth = buildAuthRequest(authInfo, config.userId, config.serviceName, config.password);
1024 json = rpcPost(gson.toJson(req));
1025 } catch (ShellyApiException e) {
1026 ShellyApiResult res = e.getApiResult();
1027 String auth = getString(res.authResponse);
1028 if (res.isHttpAccessUnauthorized() && !auth.isEmpty()) {
1029 String[] options = auth.split(",");
1030 for (String o : options) {
1031 String key = substringBefore(o, "=").stripLeading().trim();
1032 String value = substringAfter(o, "=").replaceAll("\"", "").trim();
1037 authInfo.realm = value;
1040 authInfo.nonce = Long.parseLong(value, 16);
1043 authInfo.algorithm = value;
1048 req.auth = buildAuthRequest(authInfo, config.userId, authInfo.realm, config.password);
1049 json = rpcPost(gson.toJson(req));
1054 Shelly2RpcBaseMessage response = gson.fromJson(json, Shelly2RpcBaseMessage.class);
1055 if (response == null) {
1056 throw new IllegalArgumentException("Unable to cover API result to obhect");
1058 if (response.result != null) {
1059 // return sub element result as requested class type
1060 json = gson.toJson(gson.fromJson(json, Shelly2RpcBaseMessage.class).result);
1061 return fromJson(gson, json, classOfT);
1063 // return direct format
1064 return gson.fromJson(json, classOfT);
1068 public <T> T apiRequest(Shelly2RpcRequest request, Class<T> classOfT) throws ShellyApiException {
1069 return apiRequest(request.method, request.params, classOfT);
1072 public String apiRequest(Shelly2RpcRequest request) throws ShellyApiException {
1073 return apiRequest(request.method, request.params, String.class);
1076 private String rpcPost(String postData) throws ShellyApiException {
1077 return httpPost("/rpc", postData);
1080 private void reconnect() throws ShellyApiException {
1081 if (!rpcSocket.isConnected()) {
1082 logger.debug("{}: Connect Rpc Socket (discovery = {})", thingName, discovery);
1083 rpcSocket.connect();
1087 private void disconnect() {
1088 if (rpcSocket.isConnected()) {
1089 rpcSocket.disconnect();
1093 public Shelly2RpctInterface getRpcHandler() {
1098 public void close() {
1099 logger.debug("{}: Closing Rpc API (socket is {}, discovery={})", thingName,
1100 rpcSocket.isConnected() ? "connected" : "disconnected", discovery);
1102 initialized = false;
1105 private void incProtErrors() {
1106 if (thing != null) {
1107 thing.incProtErrors();