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.util.ArrayList;
21 import java.util.List;
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;
77 * {@link Shelly2ApiRpc} implements Gen2 RPC interface
79 * @author Markus Michels - Initial contribution
82 public class Shelly2ApiRpc extends Shelly2ApiClient implements ShellyApiInterface, Shelly2RpctInterface {
83 private final Logger logger = LoggerFactory.getLogger(Shelly2ApiRpc.class);
84 private final @Nullable ShellyThingTable thingTable;
86 private boolean initialized = false;
87 private boolean discovery = false;
88 private Shelly2RpcSocket rpcSocket = new Shelly2RpcSocket();
89 private Shelly2AuthResponse authInfo = new Shelly2AuthResponse();
92 * Regular constructor - called by Thing handler
94 * @param thingName Symbolic thing name
95 * @param thing Thing Handler (ThingHandlerInterface)
97 public Shelly2ApiRpc(String thingName, ShellyThingTable thingTable, ShellyThingInterface thing) {
98 super(thingName, thing);
99 this.thingName = thingName;
101 this.thingTable = thingTable;
103 getProfile().initFromThingType(thing.getThingType());
104 } catch (ShellyApiException e) {
105 logger.info("{}: Shelly2 API initialization failed!", thingName, e);
110 * Simple initialization - called by discovery handler
112 * @param thingName Symbolic thing name
113 * @param config Thing Configuration
114 * @param httpClient HTTP Client to be passed to ShellyHttpClient
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;
124 public void initialize() throws ShellyApiException {
126 rpcSocket = new Shelly2RpcSocket(thingName, thingTable, config.deviceIp);
127 rpcSocket.addMessageHandler(this);
130 if (rpcSocket.isConnected()) {
131 logger.debug("{}: Disconnect Rpc Socket on initialize", thingName);
138 public boolean isInitialized() {
143 public ShellyDeviceProfile getDeviceProfile(String thingType) throws ShellyApiException {
144 ShellyDeviceProfile profile = thing != null ? getProfile() : new ShellyDeviceProfile();
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);
157 if (dc.cloud != null) {
158 profile.settings.cloud.enabled = getBool(dc.cloud.enable);
160 if (dc.mqtt != null) {
161 profile.settings.mqtt.enable = getBool(dc.mqtt.enable);
163 if (dc.sys.sntp != null) {
164 profile.settings.sntp.server = dc.sys.sntp.server;
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);
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;
178 if (profile.hasRelays) {
179 profile.mode = profile.isRoller ? SHELLY_CLASS_ROLLER : SHELLY_CLASS_RELAY;
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);
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;
196 if (dc.eth != null) {
197 profile.settings.ethernet = getBool(dc.eth.enable);
199 if (dc.ble != null) {
200 profile.settings.bluetooth = getBool(dc.ble.enable);
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);
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());
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();
226 input.eventCount = 0;
227 profile.status.inputs.add(input);
228 relayStatus.inputs.add(input);
232 if (dc.em0 != null) {
233 profile.numMeters = 3;
236 if (profile.numMeters > 0) {
237 profile.status.meters = new ArrayList<>();
238 profile.status.emeters = new ArrayList<>();
239 relayStatus.meters = new ArrayList<>();
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());
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);
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;
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();
268 profile.initialized = true;
270 getStatus(); // make sure profile.status is initialized (e.g,. relay/meter status)
271 asyncApiRequest(SHELLYRPC_METHOD_GETSTATUS); // request periodic status updates from device
277 private void fillWiFiSta(@Nullable Shelly2DeviceConfigSta from, ShellySettingsWiFiNetwork to) {
278 to.enabled = from != null && !getString(from.ssid).isEmpty();
282 to.mask = from.netmask;
283 to.dns = from.nameserver;
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();
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();
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);
318 public void onNotifyStatus(Shelly2RpcNotifyStatus message) {
319 logger.debug("{}: NotifyStatus update received: {}", thingName, gson.toJson(message));
321 ShellyThingInterface t = thing;
323 logger.debug("{}: No matching thing on NotifyStatus for {}, ignore (src={}, dst={}, discovery={})",
324 thingName, thingName, message.src, message.dst, discovery);
327 if (t.isStopping()) {
328 logger.debug("{}: Thing is shutting down, ignore WebSocket message", thingName);
331 if (!t.isThingOnline() && t.getThingStatusDetail() != ThingStatusDetail.CONFIGURATION_PENDING) {
332 logger.debug("{}: Thing is not in online state/connectable, ignore NotifyStatus", thingName);
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);
346 logger.debug("{}: Error status received - {} {}", thingName, message.error.code,
347 message.error.message);
352 Shelly2NotifyStatus params = message.params;
353 if (params != null) {
354 if (getThing().getThingStatusDetail() != ThingStatusDetail.FIRMWARE_UPDATING) {
355 getThing().setThingOnline();
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);
365 status.uptime = params.sys.uptime;
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;
373 updated |= updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_ITEMP,
374 toQuantityType(getDouble(status.tmp.tC), DIGITS_NONE, SIUnits.CELSIUS));
377 profile.status = status;
379 getThing().restartWatchdog();
382 } catch (ShellyApiException e) {
383 logger.debug("{}: Unable to process status update", thingName, e);
389 public void onNotifyEvent(Shelly2RpcNotifyEvent message) {
391 logger.debug("{}: NotifyEvent received: {}", thingName, gson.toJson(message));
392 ShellyDeviceProfile profile = getProfile();
394 getThing().incProtMessages();
395 getThing().restartWatchdog();
397 for (Shelly2NotifyEvent e : message.params.events) {
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));
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);
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));
430 case SHELLY2_EVENT_CFGCHANGED:
431 logger.debug("{}: Configuration update detected, re-initialize", thingName);
432 getThing().requestUpdates(1, true); // refresh config
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");
441 case SHELLY2_EVENT_OTAPROGRESS:
442 logger.debug("{}: Firmware update in progress: {}", thingName, getString(e.msg));
443 getThing().postEvent(e.event, false);
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
451 case SHELLY2_EVENT_SLEEP:
452 logger.debug("{}: Device went to sleep mode", thingName);
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);
459 case SHELLY2_EVENT_WIFIDISCONNECTED:
460 logger.debug("{}: WiFi disconnected, reason {}", thingName, getInteger(e.reason));
461 getThing().postEvent(e.event, false);
464 logger.debug("{}: Event {} was not handled", thingName, e.event);
467 } catch (ShellyApiException e) {
468 logger.debug("{}: Unable to process event", thingName, e);
474 public void onMessage(String message) {
475 logger.debug("{}: Unexpected RPC message received: {}", thingName, message);
480 public void onClose(int statusCode, String reason) {
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");
486 } catch (ShellyApiException e) {
487 logger.debug("{}: Exception on onClose()", thingName, e);
493 public void onError(Throwable cause) {
494 logger.debug("{}: WebSocket error", thingName);
495 if (thing != null && thing.getProfile().alwaysOn) {
496 thingOffline("WebSocket error");
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",
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);
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);
537 if (ds.sys.wakeupPeriod != null) {
538 profile.settings.sleepMode.period = ds.sys.wakeupPeriod / 60;
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);
548 if (ds.sys.availableUpdates.beta != null) {
549 status.update.betaVersion = "v" + getString(ds.sys.availableUpdates.beta.version);
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);
558 // Index 0 is aggregated status, 1 boot, 2 cause
559 String reason = boot.equals(SHELLY2_WAKEUPO_BOOT_RESTART) ? ALARM_TYPE_RESTARTED : cause;
561 values.add(ds.sys.wakeUpReason.boot);
562 values.add(ds.sys.wakeUpReason.cause);
563 getThing().updateWakeupReason(values);
566 fillDeviceStatus(status, ds, false);
571 public void setSleepTime(int value) throws ShellyApiException {
575 public ShellyStatusRelay getRelayStatus(int relayIndex) throws ShellyApiException {
576 if (getProfile().status.wifiSta.ssid == null) {
577 // Update status when not yet initialized
584 public void setRelayTurn(int id, String turnMode) throws ShellyApiException {
585 Shelly2RpcRequestParams params = new Shelly2RpcRequestParams();
587 params.on = SHELLY_API_ON.equals(turnMode);
588 apiRequest(SHELLYRPC_METHOD_SWITCH_SET, params, String.class);
592 public ShellyRollerStatus getRollerStatus(int rollerIndex) throws ShellyApiException {
593 if (rollerIndex < rollerStatus.size()) {
594 return rollerStatus.get(rollerIndex);
596 throw new IllegalArgumentException("Invalid rollerIndex on getRollerStatus");
600 public void setRollerTurn(int relayIndex, String turnMode) throws ShellyApiException {
601 String operation = "";
603 case SHELLY_ALWD_ROLLER_TURN_OPEN:
604 operation = SHELLY2_COVER_CMD_OPEN;
606 case SHELLY_ALWD_ROLLER_TURN_CLOSE:
607 operation = SHELLY2_COVER_CMD_CLOSE;
609 case SHELLY_ALWD_ROLLER_TURN_STOP:
610 operation = SHELLY2_COVER_CMD_STOP;
614 apiRequest(new Shelly2RpcRequest().withMethod("Cover." + operation).withId(relayIndex));
618 public void setRollerPos(int relayIndex, int position) throws ShellyApiException {
620 new Shelly2RpcRequest().withMethod(SHELLYRPC_METHOD_COVER_SETPOS).withId(relayIndex).withPos(position));
624 public ShellyStatusSensor getSensorStatus() throws ShellyApiException {
629 public void setAutoTimer(int index, String timerName, double value) throws ShellyApiException {
630 Shelly2RpcRequest req = new Shelly2RpcRequest().withMethod(SHELLYRPC_METHOD_SWITCH_SETCONFIG).withId(index);
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;
638 req.params.config.autoOff = value > 0;
639 req.params.config.autoOffDelay = value;
645 public void resetMeterTotal(int id) throws ShellyApiException {
649 public void muteSmokeAlarm(int index) throws ShellyApiException {
650 apiRequest(new Shelly2RpcRequest().withMethod(SHELLYRPC_METHOD_SMOKE_MUTE).withId(index));
654 public ShellySettingsLogin getLoginSettings() throws ShellyApiException {
655 return new ShellySettingsLogin();
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);
666 ShellySettingsLogin res = new ShellySettingsLogin();
668 res.username = params.user;
669 res.password = password;
670 return new ShellySettingsLogin();
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;
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;
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;
700 public String deviceReboot() throws ShellyApiException {
701 return apiRequest(SHELLYRPC_METHOD_REBOOT, null, String.class);
705 public String factoryReset() throws ShellyApiException {
706 return apiRequest(SHELLYRPC_METHOD_RESET, null, String.class);
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";
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");
724 Shelly2RpcRequestParams params = new Shelly2RpcRequestParams();
726 params.stage = prod || beta ? "stable" : "beta";
730 apiRequest(SHELLYRPC_METHOD_UPDATE, params, String.class);
731 res.status = "Update initiated";
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";
744 public String setDebug(boolean enabled) throws ShellyApiException {
749 public String getDebugLog(String id) throws ShellyApiException {
750 return ""; // Gen2 uses WS to publish debug log
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)
758 public void setLedStatus(String ledName, Boolean value) throws ShellyApiException {
759 throw new ShellyApiException("API call not implemented");
763 public ShellyStatusLight getLightStatus() throws ShellyApiException {
764 throw new ShellyApiException("API call not implemented");
768 public ShellyShortLightStatus getLightStatus(int index) throws ShellyApiException {
769 throw new ShellyApiException("API call not implemented");
773 public void setLightParm(int lightIndex, String parm, String value) throws ShellyApiException {
774 throw new ShellyApiException("API call not implemented");
778 public void setLightParms(int lightIndex, Map<String, String> parameters) throws ShellyApiException {
779 throw new ShellyApiException("API call not implemented");
783 public ShellyShortLightStatus setLightTurn(int id, String turnMode) throws ShellyApiException {
784 throw new ShellyApiException("API call not implemented");
788 public void setBrightness(int id, int brightness, boolean autoOn) throws ShellyApiException {
789 throw new ShellyApiException("API call not implemented");
793 public void setLightMode(String mode) throws ShellyApiException {
794 throw new ShellyApiException("API call not implemented");
798 public void setValveMode(int valveId, boolean auto) throws ShellyApiException {
799 throw new ShellyApiException("API call not implemented");
803 public void setValvePosition(int valveId, double value) throws ShellyApiException {
804 throw new ShellyApiException("API call not implemented");
808 public void setValveTemperature(int valveId, int value) throws ShellyApiException {
809 throw new ShellyApiException("API call not implemented");
813 public void setValveProfile(int valveId, int value) throws ShellyApiException {
814 throw new ShellyApiException("API call not implemented");
818 public void setValveBoostTime(int valveId, int value) throws ShellyApiException {
819 throw new ShellyApiException("API call not implemented");
823 public void startValveBoost(int valveId, int value) throws ShellyApiException {
824 throw new ShellyApiException("API call not implemented");
828 public String resetStaCache() throws ShellyApiException {
829 throw new ShellyApiException("API call not implemented");
833 public void setActionURLs() throws ShellyApiException {
834 // not relevant for Gen2
838 public ShellySettingsLogin setCoIoTPeer(String peer) throws ShellyApiException {
839 // not relevant for Gen2
840 return new ShellySettingsLogin();
844 public String getCoIoTDescription() {
845 return ""; // not relevant to Gen2
849 public void sendIRKey(String keyCode) throws ShellyApiException, IllegalArgumentException {
850 throw new ShellyApiException("API call not implemented");
854 public String setWiFiRecovery(boolean enable) throws ShellyApiException {
855 return "failed"; // not supported by Gen2
859 public String setApRoaming(boolean enable) throws ShellyApiException {
860 return "false";// not supported by Gen2
863 private void asyncApiRequest(String method) throws ShellyApiException {
864 Shelly2RpcBaseMessage request = buildRequest(method, null);
866 rpcSocket.sendMessage(gson.toJson(request)); // submit, result wull be async
869 public <T> T apiRequest(String method, @Nullable Object params, Class<T> classOfT) throws ShellyApiException {
871 Shelly2RpcBaseMessage req = buildRequest(method, params);
873 reconnect(); // make sure WS is connected
875 if (authInfo.realm != null) {
876 req.auth = buildAuthRequest(authInfo, config.userId, config.serviceName, config.password);
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();
891 authInfo.realm = value;
894 authInfo.nonce = Long.parseLong(value, 16);
897 authInfo.algorithm = value;
902 req.auth = buildAuthRequest(authInfo, config.userId, authInfo.realm, config.password);
903 json = rpcPost(gson.toJson(req));
908 json = gson.toJson(gson.fromJson(json, Shelly2RpcBaseMessage.class).result);
909 return fromJson(gson, json, classOfT);
912 public <T> T apiRequest(Shelly2RpcRequest request, Class<T> classOfT) throws ShellyApiException {
913 return apiRequest(request.method, request.params, classOfT);
916 public String apiRequest(Shelly2RpcRequest request) throws ShellyApiException {
917 return apiRequest(request.method, request.params, String.class);
920 private String rpcPost(String postData) throws ShellyApiException {
921 return httpPost("/rpc", postData);
924 private void reconnect() throws ShellyApiException {
925 if (!rpcSocket.isConnected()) {
926 logger.debug("{}: Connect Rpc Socket (discovery = {})", thingName, discovery);
931 private void disconnect() {
932 if (rpcSocket.isConnected()) {
933 rpcSocket.disconnect();
937 public Shelly2RpctInterface getRpcHandler() {
942 public void close() {
943 logger.debug("{}: Closing Rpc API (socket is {}, discovery={})", thingName,
944 rpcSocket.isConnected() ? "connected" : "disconnected", discovery);
949 private void incProtErrors() {
951 thing.incProtErrors();