2 * Copyright (c) 2010-2021 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.api;
15 import static org.openhab.binding.shelly.internal.ShellyBindingConstants.*;
16 import static org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.*;
17 import static org.openhab.binding.shelly.internal.util.ShellyUtils.*;
19 import java.nio.charset.StandardCharsets;
20 import java.util.Base64;
21 import java.util.HashMap;
23 import java.util.concurrent.ExecutionException;
24 import java.util.concurrent.TimeUnit;
25 import java.util.concurrent.TimeoutException;
27 import org.eclipse.jdt.annotation.NonNullByDefault;
28 import org.eclipse.jetty.client.HttpClient;
29 import org.eclipse.jetty.client.api.ContentResponse;
30 import org.eclipse.jetty.client.api.Request;
31 import org.eclipse.jetty.http.HttpHeader;
32 import org.eclipse.jetty.http.HttpMethod;
33 import org.eclipse.jetty.http.HttpStatus;
34 import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellyControlRoller;
35 import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySendKeyList;
36 import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySenseKeyCode;
37 import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsDevice;
38 import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsLight;
39 import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsLogin;
40 import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsStatus;
41 import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsUpdate;
42 import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellyShortLightStatus;
43 import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellyStatusLight;
44 import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellyStatusRelay;
45 import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellyStatusSensor;
46 import org.openhab.binding.shelly.internal.config.ShellyThingConfiguration;
47 import org.openhab.core.library.unit.ImperialUnits;
48 import org.openhab.core.library.unit.SIUnits;
49 import org.slf4j.Logger;
50 import org.slf4j.LoggerFactory;
52 import com.google.gson.Gson;
53 import com.google.gson.JsonSyntaxException;
56 * {@link ShellyHttpApi} wraps the Shelly REST API and provides various low level function to access the device api (not
59 * @author Markus Michels - Initial contribution
62 public class ShellyHttpApi {
63 public static final String HTTP_HEADER_AUTH = "Authorization";
64 public static final String HTTP_AUTH_TYPE_BASIC = "Basic";
65 public static final String CONTENT_TYPE_JSON = "application/json; charset=UTF-8";
67 private final Logger logger = LoggerFactory.getLogger(ShellyHttpApi.class);
68 private final HttpClient httpClient;
69 private ShellyThingConfiguration config = new ShellyThingConfiguration();
70 private String thingName;
71 private final Gson gson = new Gson();
72 private int timeoutErrors = 0;
73 private int timeoutsRecovered = 0;
75 private ShellyDeviceProfile profile = new ShellyDeviceProfile();
77 public ShellyHttpApi(String thingName, ShellyThingConfiguration config, HttpClient httpClient) {
78 this.httpClient = httpClient;
79 this.thingName = thingName;
80 setConfig(thingName, config);
81 profile.initFromThingType(thingName);
84 public void setConfig(String thingName, ShellyThingConfiguration config) {
85 this.thingName = thingName;
89 public ShellySettingsDevice getDevInfo() throws ShellyApiException {
90 return callApi(SHELLY_URL_DEVINFO, ShellySettingsDevice.class);
94 * Initialize the device profile
96 * @param thingType Type of DEVICE as returned from the thing properties (based on discovery)
97 * @return Initialized ShellyDeviceProfile
98 * @throws ShellyApiException
100 public ShellyDeviceProfile getDeviceProfile(String thingType) throws ShellyApiException {
101 String json = request(SHELLY_URL_SETTINGS);
102 if (json.contains("\"type\":\"SHDM-")) {
103 logger.trace("{}: Detected a Shelly Dimmer: fix Json (replace lights[] tag with dimmers[]", thingName);
104 json = fixDimmerJson(json);
107 // Map settings to device profile for Light and Sense
108 profile.initialize(thingType, json);
110 // 2nd level initialization
111 profile.thingName = profile.hostname;
112 if (profile.isLight && (profile.numMeters == 0)) {
113 logger.debug("{}: Get number of meters from light status", thingName);
114 ShellyStatusLight status = getLightStatus();
115 profile.numMeters = status.meters != null ? status.meters.size() : 0;
117 if (profile.isSense) {
118 profile.irCodes = getIRCodeList();
119 logger.debug("{}: Sense stored key list loaded, {} entries.", thingName, profile.irCodes.size());
125 public boolean isInitialized() {
126 return profile.initialized;
130 * Get generic device settings/status. Json returned from API will be mapped to a Gson object
132 * @return Device settings/status as ShellySettingsStatus object
133 * @throws ShellyApiException
135 public ShellySettingsStatus getStatus() throws ShellyApiException {
138 json = request(SHELLY_URL_STATUS);
139 // Dimmer2 returns invalid json type for loaderror :-(
140 json = getString(json.replace("\"loaderror\":0,", "\"loaderror\":false,"));
141 json = getString(json.replace("\"loaderror\":1,", "\"loaderror\":true,"));
142 ShellySettingsStatus status = fromJson(gson, json, ShellySettingsStatus.class);
145 } catch (JsonSyntaxException e) {
146 throw new ShellyApiException("Unable to parse JSON: " + json, e);
150 public ShellyStatusRelay getRelayStatus(Integer relayIndex) throws ShellyApiException {
151 return callApi(SHELLY_URL_STATUS_RELEAY + "/" + relayIndex.toString(), ShellyStatusRelay.class);
154 public ShellyShortLightStatus setRelayTurn(Integer id, String turnMode) throws ShellyApiException {
155 return callApi(getControlUriPrefix(id) + "?" + SHELLY_LIGHT_TURN + "=" + turnMode.toLowerCase(),
156 ShellyShortLightStatus.class);
159 public void setBrightness(Integer id, Integer brightness, boolean autoOn) throws ShellyApiException {
160 String turn = autoOn ? SHELLY_LIGHT_TURN + "=" + SHELLY_API_ON + "&" : "";
161 request(getControlUriPrefix(id) + "?" + turn + "brightness=" + brightness.toString());
164 public ShellyControlRoller getRollerStatus(Integer rollerIndex) throws ShellyApiException {
165 String uri = SHELLY_URL_CONTROL_ROLLER + "/" + rollerIndex.toString() + "/pos";
166 return callApi(uri, ShellyControlRoller.class);
169 public void setRollerTurn(Integer relayIndex, String turnMode) throws ShellyApiException {
170 request(SHELLY_URL_CONTROL_ROLLER + "/" + relayIndex.toString() + "?go=" + turnMode);
173 public void setRollerPos(Integer relayIndex, Integer position) throws ShellyApiException {
174 request(SHELLY_URL_CONTROL_ROLLER + "/" + relayIndex.toString() + "?go=to_pos&roller_pos="
175 + position.toString());
178 public void setRollerTimer(Integer relayIndex, Integer timer) throws ShellyApiException {
179 request(SHELLY_URL_CONTROL_ROLLER + "/" + relayIndex.toString() + "?timer=" + timer.toString());
182 public ShellyShortLightStatus getLightStatus(Integer index) throws ShellyApiException {
183 return callApi(getControlUriPrefix(index), ShellyShortLightStatus.class);
186 public ShellyStatusSensor getSensorStatus() throws ShellyApiException {
187 ShellyStatusSensor status = callApi(SHELLY_URL_STATUS, ShellyStatusSensor.class);
188 if (profile.isSense) {
189 // complete reported data, map C to F or vice versa: C=(F - 32) * 0.5556;
190 status.tmp.tC = status.tmp.units.equals(SHELLY_TEMP_CELSIUS) ? status.tmp.value
191 : ImperialUnits.FAHRENHEIT.getConverterTo(SIUnits.CELSIUS).convert(getDouble(status.tmp.value))
193 double f = (double) SIUnits.CELSIUS.getConverterTo(ImperialUnits.FAHRENHEIT)
194 .convert(getDouble(status.tmp.value));
195 status.tmp.tF = status.tmp.units.equals(SHELLY_TEMP_FAHRENHEIT) ? status.tmp.value : f;
197 if ((status.charger == null) && (status.externalPower != null)) {
198 // SHelly H&T uses external_power, Sense uses charger
199 status.charger = status.externalPower != 0;
205 public void setTimer(Integer index, String timerName, Double value) throws ShellyApiException {
206 String type = SHELLY_CLASS_RELAY;
207 if (profile.isRoller) {
208 type = SHELLY_CLASS_ROLLER;
209 } else if (profile.isLight) {
210 type = SHELLY_CLASS_LIGHT;
212 String uri = SHELLY_URL_SETTINGS + "/" + type + "/" + index + "?" + timerName + "="
213 + ((Integer) value.intValue()).toString();
217 public void setLedStatus(String ledName, Boolean value) throws ShellyApiException {
218 request(SHELLY_URL_SETTINGS + "?" + ledName + "=" + (value ? SHELLY_API_TRUE : SHELLY_API_FALSE));
221 public ShellySettingsLight getLightSettings() throws ShellyApiException {
222 return callApi(SHELLY_URL_SETTINGS_LIGHT, ShellySettingsLight.class);
225 public ShellyStatusLight getLightStatus() throws ShellyApiException {
226 return callApi(SHELLY_URL_STATUS, ShellyStatusLight.class);
229 public void setLightSetting(String parm, String value) throws ShellyApiException {
230 request(SHELLY_URL_SETTINGS + "?" + parm + "=" + value);
233 public ShellySettingsLogin getLoginSettings() throws ShellyApiException {
234 return callApi(SHELLY_URL_SETTINGS + "/login", ShellySettingsLogin.class);
237 public ShellySettingsLogin setLoginCredentials(String user, String password) throws ShellyApiException {
238 return callApi(SHELLY_URL_SETTINGS + "/login?enabled=yes&username=" + user + "&password=" + password,
239 ShellySettingsLogin.class);
242 public String deviceReboot() throws ShellyApiException {
243 return callApi(SHELLY_URL_RESTART, String.class);
246 public String factoryReset() throws ShellyApiException {
247 return callApi(SHELLY_URL_SETTINGS + "?reset=true", String.class);
250 public ShellySettingsUpdate firmwareUpdate(String uri) throws ShellyApiException {
251 return callApi("/ota?" + uri, ShellySettingsUpdate.class);
255 * Change between White and Color Mode
258 * @throws ShellyApiException
260 public void setLightMode(String mode) throws ShellyApiException {
261 if (!mode.isEmpty() && !profile.mode.equals(mode)) {
262 setLightSetting(SHELLY_API_MODE, mode);
264 profile.inColor = profile.isLight && profile.mode.equalsIgnoreCase(SHELLY_MODE_COLOR);
269 * Set a single light parameter
271 * @param lightIndex Index of the light, usually 0 for Bulb and 0..3 for RGBW2.
272 * @param parm Name of the parameter (see API spec)
273 * @param value The value
274 * @throws ShellyApiException
276 public void setLightParm(Integer lightIndex, String parm, String value) throws ShellyApiException {
277 // Bulb, RGW2: /<color mode>/<light id>?parm?value
278 // Dimmer: /light/<light id>?parm=value
279 request(getControlUriPrefix(lightIndex) + "?" + parm + "=" + value);
282 public void setLightParms(Integer lightIndex, Map<String, String> parameters) throws ShellyApiException {
283 String url = getControlUriPrefix(lightIndex) + "?";
285 for (String key : parameters.keySet()) {
289 url = url + key + "=" + parameters.get(key);
296 * Retrieve the IR Code list from the Shelly Sense device. The list could be customized by the user. It defines the
297 * symbolic key code, which gets
298 * map into a PRONTO code
300 * @return Map of key codes
301 * @throws ShellyApiException
303 public Map<String, String> getIRCodeList() throws ShellyApiException {
304 String result = request(SHELLY_URL_LIST_IR);
305 // take pragmatic approach to make the returned JSon into named arrays for Gson parsing
306 String keyList = substringAfter(result, "[");
307 keyList = substringBeforeLast(keyList, "]");
308 keyList = keyList.replaceAll(java.util.regex.Pattern.quote("\",\""), "\", \"name\": \"");
309 keyList = keyList.replaceAll(java.util.regex.Pattern.quote("["), "{ \"id\":");
310 keyList = keyList.replaceAll(java.util.regex.Pattern.quote("]"), "} ");
311 String json = "{\"key_codes\" : [" + keyList + "] }";
312 ShellySendKeyList codes = fromJson(gson, json, ShellySendKeyList.class);
313 Map<String, String> list = new HashMap<>();
314 for (ShellySenseKeyCode key : codes.keyCodes) {
316 list.put(key.id, key.name);
323 * Sends a IR key code to the Shelly Sense.
325 * @param keyCode A keyCoud could be a symbolic name (as defined in the key map on the device) or a PRONTO Code in
326 * plain or hex64 format
328 * @throws ShellyApiException
329 * @throws IllegalArgumentException
331 public void sendIRKey(String keyCode) throws ShellyApiException, IllegalArgumentException {
333 if (profile.irCodes.containsKey(keyCode)) {
334 type = SHELLY_IR_CODET_STORED;
335 } else if ((keyCode.length() > 4) && keyCode.contains(" ")) {
336 type = SHELLY_IR_CODET_PRONTO;
338 type = SHELLY_IR_CODET_PRONTO_HEX;
340 String url = SHELLY_URL_SEND_IR + "?type=" + type;
341 if (type.equals(SHELLY_IR_CODET_STORED)) {
342 url = url + "&" + "id=" + keyCode;
343 } else if (type.equals(SHELLY_IR_CODET_PRONTO)) {
344 String code = Base64.getEncoder().encodeToString(keyCode.getBytes(StandardCharsets.UTF_8));
345 url = url + "&" + SHELLY_IR_CODET_PRONTO + "=" + code;
346 } else if (type.equals(SHELLY_IR_CODET_PRONTO_HEX)) {
347 url = url + "&" + SHELLY_IR_CODET_PRONTO_HEX + "=" + keyCode;
352 public void setSenseSetting(String setting, String value) throws ShellyApiException {
353 request(SHELLY_URL_SETTINGS + "?" + setting + "=" + value);
357 * Set event callback URLs. Depending on the device different event types are supported. In fact all of them will be
358 * redirected to the binding's servlet and act as a trigger to schedule a status update
360 * @param ShellyApiException
361 * @throws ShellyApiException
363 public void setActionURLs() throws ShellyApiException {
366 setSensorEventUrls();
369 private void setRelayEvents() throws ShellyApiException {
370 if (profile.settings.relays != null) {
371 int num = profile.isRoller ? profile.numRollers : profile.numRelays;
372 for (int i = 0; i < num; i++) {
378 private void setDimmerEvents() throws ShellyApiException {
379 if (profile.settings.dimmers != null) {
380 for (int i = 0; i < profile.settings.dimmers.size(); i++) {
383 } else if (profile.isLight) {
389 * Set sensor Action URLs
391 * @throws ShellyApiException
393 private void setSensorEventUrls() throws ShellyApiException, ShellyApiException {
394 if (profile.isSensor) {
395 logger.debug("{}: Set Sensor Reporting URL", thingName);
396 setEventUrl(config.eventsSensorReport, SHELLY_EVENT_SENSORREPORT, SHELLY_EVENT_DARK, SHELLY_EVENT_TWILIGHT,
397 SHELLY_EVENT_FLOOD_DETECTED, SHELLY_EVENT_FLOOD_GONE, SHELLY_EVENT_OPEN, SHELLY_EVENT_CLOSE,
398 SHELLY_EVENT_VIBRATION, SHELLY_EVENT_ALARM_MILD, SHELLY_EVENT_ALARM_HEAVY, SHELLY_EVENT_ALARM_OFF,
399 SHELLY_EVENT_TEMP_OVER, SHELLY_EVENT_TEMP_UNDER);
404 * Set/delete Relay/Roller/Dimmer Action URLs
406 * @param index Device Index (0-based)
407 * @throws ShellyApiException
409 private void setEventUrls(Integer index) throws ShellyApiException {
410 if (profile.isRoller) {
411 setEventUrl(EVENT_TYPE_ROLLER, 0, config.eventsRoller, SHELLY_EVENT_ROLLER_OPEN, SHELLY_EVENT_ROLLER_CLOSE,
412 SHELLY_EVENT_ROLLER_STOP);
413 } else if (profile.isDimmer) {
415 setEventUrl(EVENT_TYPE_LIGHT, index, config.eventsButton, SHELLY_EVENT_BTN1_ON, SHELLY_EVENT_BTN1_OFF,
416 SHELLY_EVENT_BTN2_ON, SHELLY_EVENT_BTN2_OFF);
417 setEventUrl(EVENT_TYPE_LIGHT, index, config.eventsPush, SHELLY_EVENT_SHORTPUSH1, SHELLY_EVENT_LONGPUSH1,
418 SHELLY_EVENT_SHORTPUSH2, SHELLY_EVENT_LONGPUSH2);
421 setEventUrl(EVENT_TYPE_LIGHT, index, config.eventsSwitch, SHELLY_EVENT_OUT_ON, SHELLY_EVENT_OUT_OFF);
422 } else if (profile.hasRelays) {
423 // Standard relays: btn_xxx, out_xxx, short/longpush URLs
424 setEventUrl(EVENT_TYPE_RELAY, index, config.eventsButton, SHELLY_EVENT_BTN_ON, SHELLY_EVENT_BTN_OFF);
425 setEventUrl(EVENT_TYPE_RELAY, index, config.eventsPush, SHELLY_EVENT_SHORTPUSH, SHELLY_EVENT_LONGPUSH);
426 setEventUrl(EVENT_TYPE_RELAY, index, config.eventsSwitch, SHELLY_EVENT_OUT_ON, SHELLY_EVENT_OUT_OFF);
427 } else if (profile.isLight) {
429 setEventUrl(EVENT_TYPE_LIGHT, index, config.eventsSwitch, SHELLY_EVENT_OUT_ON, SHELLY_EVENT_OUT_OFF);
433 private void setEventUrl(boolean enabled, String... eventTypes) throws ShellyApiException {
434 if (config.localIp.isEmpty()) {
435 throw new ShellyApiException(thingName + ": Local IP address was not detected, can't build Callback URL");
437 for (String eventType : eventTypes) {
438 if (profile.containsEventUrl(eventType)) {
439 // H&T adds the type=xx to report_url itself, so we need to ommit here
440 String eclass = profile.isSensor ? EVENT_TYPE_SENSORDATA : eventType;
441 String urlParm = eventType.contains("temp") || profile.isHT ? "" : "?type=" + eventType;
442 String callBackUrl = "http://" + config.localIp + ":" + config.localPort + SHELLY_CALLBACK_URI + "/"
443 + profile.thingName + "/" + eclass + urlParm;
444 String newUrl = enabled ? callBackUrl : SHELLY_NULL_URL;
445 String testUrl = "\"" + mkEventUrl(eventType) + "\":\"" + newUrl + "\"";
446 if (!enabled && !profile.settingsJson.contains(testUrl)) {
447 // Don't set URL to null when the current one doesn't point to this OH
448 // Don't interfere with a 3rd party App
451 if (!profile.settingsJson.contains(testUrl)) {
452 // Current Action URL is != new URL
453 logger.debug("{}: Set new url for event type {}: {}", thingName, eventType, newUrl);
454 request(SHELLY_URL_SETTINGS + "?" + mkEventUrl(eventType) + "=" + urlEncode(newUrl));
460 private void setEventUrl(String deviceClass, Integer index, boolean enabled, String... eventTypes)
461 throws ShellyApiException {
462 for (String eventType : eventTypes) {
463 if (profile.containsEventUrl(eventType)) {
464 String callBackUrl = "http://" + config.localIp + ":" + config.localPort + SHELLY_CALLBACK_URI + "/"
465 + profile.thingName + "/" + deviceClass + "/" + index + "?type=" + eventType;
466 String newUrl = enabled ? callBackUrl : SHELLY_NULL_URL;
467 String test = "\"" + mkEventUrl(eventType) + "\":\"" + callBackUrl + "\"";
468 if (!enabled && !profile.settingsJson.contains(test)) {
469 // Don't set URL to null when the current one doesn't point to this OH
470 // Don't interfere with a 3rd party App
473 test = "\"" + mkEventUrl(eventType) + "\":\"" + newUrl + "\"";
474 if (!profile.settingsJson.contains(test)) {
475 // Current Action URL is != new URL
476 logger.debug("{}: Set URL for type {} to {}", thingName, eventType, newUrl);
477 request(SHELLY_URL_SETTINGS + "/" + deviceClass + "/" + index + "?" + mkEventUrl(eventType) + "="
478 + urlEncode(newUrl));
484 private static String mkEventUrl(String eventType) {
485 return eventType + SHELLY_EVENTURL_SUFFIX;
489 * Submit GET request and return response, check for invalid responses
491 * @param uri: URI (e.g. "/settings")
493 public <T> T callApi(String uri, Class<T> classOfT) throws ShellyApiException {
494 String json = request(uri);
495 return fromJson(gson, json, classOfT);
498 private String request(String uri) throws ShellyApiException {
499 ShellyApiResult apiResult = new ShellyApiResult();
501 boolean timeout = false;
502 while (retries > 0) {
504 apiResult = innerRequest(HttpMethod.GET, uri);
506 logger.debug("{}: API timeout #{}/{} recovered ({})", thingName, timeoutErrors, timeoutsRecovered,
510 return apiResult.response; // successful
511 } catch (ShellyApiException e) {
512 if ((!e.isTimeout() && !apiResult.isHttpServerError()) || profile.hasBattery || (retries == 0)) {
513 // Sensor in sleep mode or API exception for non-battery device or retry counter expired
514 throw e; // non-timeout exception
519 timeoutErrors++; // count the retries
520 logger.debug("{}: API Timeout, retry #{} ({})", thingName, timeoutErrors, e.toString());
523 throw new ShellyApiException("API Timeout or inconsistent result"); // successful
526 private ShellyApiResult innerRequest(HttpMethod method, String uri) throws ShellyApiException {
527 Request request = null;
528 String url = "http://" + config.deviceIp + uri;
529 ShellyApiResult apiResult = new ShellyApiResult(method.toString(), url);
532 request = httpClient.newRequest(url).method(method.toString()).timeout(SHELLY_API_TIMEOUT_MS,
533 TimeUnit.MILLISECONDS);
535 if (!config.userId.isEmpty()) {
536 String value = config.userId + ":" + config.password;
537 request.header(HTTP_HEADER_AUTH,
538 HTTP_AUTH_TYPE_BASIC + " " + Base64.getEncoder().encodeToString(value.getBytes()));
540 request.header(HttpHeader.ACCEPT, CONTENT_TYPE_JSON);
541 logger.trace("{}: HTTP {} for {}", thingName, method, url);
543 // Do request and get response
544 ContentResponse contentResponse = request.send();
545 apiResult = new ShellyApiResult(contentResponse);
546 String response = contentResponse.getContentAsString().replace("\t", "").replace("\r\n", "").trim();
547 logger.trace("{}: HTTP Response {}: {}", thingName, contentResponse.getStatus(), response);
549 // validate response, API errors are reported as Json
550 if (contentResponse.getStatus() != HttpStatus.OK_200) {
551 throw new ShellyApiException(apiResult);
553 if (response.isEmpty() || !response.startsWith("{") && !response.startsWith("[")) {
554 throw new ShellyApiException("Unexpected response: " + response);
556 } catch (ExecutionException | InterruptedException | TimeoutException | IllegalArgumentException e) {
557 ShellyApiException ex = new ShellyApiException(apiResult, e);
558 if (!ex.isTimeout()) { // will be handled by the caller
559 logger.trace("{}: API call returned exception", thingName, ex);
566 public String getControlUriPrefix(Integer id) {
568 if (profile.isLight || profile.isDimmer) {
569 if (profile.isDuo || profile.isDimmer) {
571 uri = SHELLY_URL_CONTROL_LIGHT;
574 uri = "/" + (profile.inColor ? SHELLY_MODE_COLOR : SHELLY_MODE_WHITE);
578 uri = SHELLY_URL_CONTROL_RELEAY;
580 uri = uri + "/" + id;
584 public int getTimeoutErrors() {
585 return timeoutErrors;
588 public int getTimeoutsRecovered() {
589 return timeoutsRecovered;