]> git.basschouten.com Git - openhab-addons.git/blob
0151e29679f0f512981a3d4530f2b497a62f3c86
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2024 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
7  * This program and the accompanying materials are made available under the
8  * terms of the Eclipse Public License 2.0 which is available at
9  * http://www.eclipse.org/legal/epl-2.0
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.shelly.internal.api1;
14
15 import static org.openhab.binding.shelly.internal.ShellyBindingConstants.*;
16 import static org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.*;
17 import static org.openhab.binding.shelly.internal.util.ShellyUtils.*;
18
19 import java.nio.charset.StandardCharsets;
20 import java.util.Base64;
21 import java.util.HashMap;
22 import java.util.Map;
23
24 import org.eclipse.jdt.annotation.NonNullByDefault;
25 import org.eclipse.jdt.annotation.Nullable;
26 import org.eclipse.jetty.client.HttpClient;
27 import org.openhab.binding.shelly.internal.api.ShellyApiException;
28 import org.openhab.binding.shelly.internal.api.ShellyApiInterface;
29 import org.openhab.binding.shelly.internal.api.ShellyDeviceProfile;
30 import org.openhab.binding.shelly.internal.api.ShellyHttpClient;
31 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyOtaCheckResult;
32 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyRollerStatus;
33 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySendKeyList;
34 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySenseKeyCode;
35 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsDevice;
36 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsLight;
37 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsLogin;
38 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsStatus;
39 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsUpdate;
40 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyShortLightStatus;
41 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyStatusLight;
42 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyStatusRelay;
43 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyStatusSensor;
44 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyThermnostat;
45 import org.openhab.binding.shelly.internal.config.ShellyThingConfiguration;
46 import org.openhab.binding.shelly.internal.handler.ShellyThingInterface;
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;
51
52 import com.google.gson.JsonSyntaxException;
53
54 /**
55  * {@link Shelly1HttpApi} wraps the Shelly REST API and provides various low level function to access the device api
56  * (not
57  * cloud api).
58  *
59  * @author Markus Michels - Initial contribution
60  */
61 @NonNullByDefault
62 public class Shelly1HttpApi extends ShellyHttpClient implements ShellyApiInterface {
63     private final Logger logger = LoggerFactory.getLogger(Shelly1HttpApi.class);
64     private final ShellyDeviceProfile profile;
65
66     public Shelly1HttpApi(String thingName, ShellyThingInterface thing) {
67         super(thingName, thing);
68         profile = thing.getProfile();
69     }
70
71     /**
72      * Simple initialization - called by discovery handler
73      *
74      * @param thingName Symbolic thing name
75      * @param config Thing Configuration
76      * @param httpClient HTTP Client to be passed to ShellyHttpClient
77      */
78     public Shelly1HttpApi(String thingName, ShellyThingConfiguration config, HttpClient httpClient) {
79         super(thingName, config, httpClient);
80         this.profile = new ShellyDeviceProfile();
81     }
82
83     @Override
84     public void initialize() throws ShellyApiException {
85         profile.device = getDeviceInfo();
86     }
87
88     @Override
89     public ShellySettingsDevice getDeviceInfo() throws ShellyApiException {
90         ShellySettingsDevice info = callApi(SHELLY_URL_DEVINFO, ShellySettingsDevice.class);
91         info.gen = 1;
92         basicAuth = getBool(info.auth);
93
94         if (getString(info.mode).isEmpty()) { // older Gen1 Firmware
95             if (getInteger(info.numRollers) > 0) {
96                 info.mode = SHELLY_CLASS_ROLLER;
97             } else if (getInteger(info.numOutputs) > 0) {
98                 info.mode = SHELLY_CLASS_RELAY;
99             } else {
100                 info.mode = "";
101             }
102         }
103         return info;
104     }
105
106     @Override
107     public String setDebug(boolean enabled) throws ShellyApiException {
108         return callApi(SHELLY_URL_SETTINGS + "?debug_enable=" + Boolean.valueOf(enabled), String.class);
109     }
110
111     @Override
112     public String getDebugLog(String id) throws ShellyApiException {
113         return callApi("/debug/" + id, String.class);
114     }
115
116     /**
117      * Initialize the device profile
118      *
119      * @param thingType Type of DEVICE as returned from the thing properties (based on discovery)
120      * @return Initialized ShellyDeviceProfile
121      * @throws ShellyApiException
122      */
123     @Override
124     public ShellyDeviceProfile getDeviceProfile(String thingType, @Nullable ShellySettingsDevice device)
125             throws ShellyApiException {
126         if (device != null) {
127             profile.device = device;
128         }
129         if (profile.device.type == null) {
130             profile.device = getDeviceInfo();
131         }
132         String json = httpRequest(SHELLY_URL_SETTINGS);
133         if (json.contains("\"type\":\"SHDM-")) {
134             logger.trace("{}: Detected a Shelly Dimmer: fix Json (replace lights[] tag with dimmers[]", thingName);
135             json = fixDimmerJson(json);
136         }
137
138         // Map settings to device profile for Light and Sense
139         profile.initialize(thingType, json, profile.device);
140
141         // 2nd level initialization
142         profile.thingName = profile.device.hostname;
143         if (profile.isLight && (profile.numMeters == 0)) {
144             logger.debug("{}: Get number of meters from light status", thingName);
145             ShellyStatusLight status = getLightStatus();
146             profile.numMeters = status.meters != null ? status.meters.size() : 0;
147         }
148         if (profile.isSense) {
149             profile.irCodes = getIRCodeList();
150             logger.debug("{}: Sense stored key list loaded, {} entries.", thingName, profile.irCodes.size());
151         }
152
153         return profile;
154     }
155
156     @Override
157     public boolean isInitialized() {
158         return profile.initialized;
159     }
160
161     /**
162      * Get generic device settings/status. Json returned from API will be mapped to a Gson object
163      *
164      * @return Device settings/status as ShellySettingsStatus object
165      * @throws ShellyApiException
166      */
167     @Override
168     public ShellySettingsStatus getStatus() throws ShellyApiException {
169         String json = "";
170         try {
171             json = httpRequest(SHELLY_URL_STATUS);
172
173             // Dimmer2 returns invalid json type for loaderror :-(
174             json = json.replace("\"loaderror\":0,", "\"loaderror\":false,")
175                     .replace("\"loaderror\":1,", "\"loaderror\":true,")
176                     .replace("\"tmp\":{\"value\": \"null\",", "\"tmp\":{\"value\": null,");
177             ShellySettingsStatus status = fromJson(gson, json, ShellySettingsStatus.class);
178             status.json = json;
179             return status;
180         } catch (JsonSyntaxException e) {
181             throw new ShellyApiException("Unable to parse JSON: " + json, e);
182         }
183     }
184
185     @Override
186     public ShellyStatusRelay getRelayStatus(int relayIndex) throws ShellyApiException {
187         return callApi(SHELLY_URL_STATUS_RELEAY + "/" + relayIndex, ShellyStatusRelay.class);
188     }
189
190     @Override
191     public void setRelayTurn(int id, String turnMode) throws ShellyApiException {
192         callApi(getControlUriPrefix(id) + "?" + SHELLY_LIGHT_TURN + "=" + turnMode.toLowerCase(), String.class);
193     }
194
195     @Override
196     public void resetMeterTotal(int id) throws ShellyApiException {
197         callApi(SHELLY_URL_STATUS_EMETER + "/" + id + "?reset_totals=true", ShellyStatusRelay.class);
198     }
199
200     @Override
201     public ShellyShortLightStatus setLightTurn(int id, String turnMode) throws ShellyApiException {
202         return callApi(getControlUriPrefix(id) + "?" + SHELLY_LIGHT_TURN + "=" + turnMode.toLowerCase(),
203                 ShellyShortLightStatus.class);
204     }
205
206     @Override
207     public void setBrightness(int id, int brightness, boolean autoOn) throws ShellyApiException {
208         String turn = autoOn ? SHELLY_LIGHT_TURN + "=" + SHELLY_API_ON + "&" : "";
209         httpRequest(getControlUriPrefix(id) + "?" + turn + "brightness=" + brightness);
210     }
211
212     @Override
213     public ShellyRollerStatus getRollerStatus(int rollerIndex) throws ShellyApiException {
214         String uri = SHELLY_URL_CONTROL_ROLLER + "/" + rollerIndex + "/pos";
215         return callApi(uri, ShellyRollerStatus.class);
216     }
217
218     @Override
219     public void setRollerTurn(int relayIndex, String turnMode) throws ShellyApiException {
220         httpRequest(SHELLY_URL_CONTROL_ROLLER + "/" + relayIndex + "?go=" + turnMode);
221     }
222
223     @Override
224     public void setRollerPos(int relayIndex, int position) throws ShellyApiException {
225         httpRequest(SHELLY_URL_CONTROL_ROLLER + "/" + relayIndex + "?go=to_pos&roller_pos=" + position);
226     }
227
228     @Override
229     public ShellyShortLightStatus getLightStatus(int index) throws ShellyApiException {
230         return callApi(getControlUriPrefix(index), ShellyShortLightStatus.class);
231     }
232
233     @Override
234     public ShellyStatusSensor getSensorStatus() throws ShellyApiException {
235         ShellyStatusSensor status = callApi(SHELLY_URL_STATUS, ShellyStatusSensor.class);
236         if (profile.isSense) {
237             // complete reported data, map C to F or vice versa: C=(F - 32) * 0.5556;
238             status.tmp.tC = status.tmp.units.equals(SHELLY_TEMP_CELSIUS) ? status.tmp.value
239                     : ImperialUnits.FAHRENHEIT.getConverterTo(SIUnits.CELSIUS).convert(getDouble(status.tmp.value))
240                             .doubleValue();
241             double f = (double) SIUnits.CELSIUS.getConverterTo(ImperialUnits.FAHRENHEIT)
242                     .convert(getDouble(status.tmp.value));
243             status.tmp.tF = status.tmp.units.equals(SHELLY_TEMP_FAHRENHEIT) ? status.tmp.value : f;
244         }
245         if ((status.charger == null) && (profile.settings.externalPower != null)) {
246             // SHelly H&T uses external_power, Sense uses charger
247             status.charger = profile.settings.externalPower != 0;
248         }
249         if (status.tmp != null && status.tmp.tC == null && status.tmp.value != null) { // Motion is is missing tC and tF
250             status.tmp.tC = getString(status.tmp.units).toUpperCase().equals(SHELLY_TEMP_FAHRENHEIT)
251                     ? ImperialUnits.FAHRENHEIT.getConverterTo(SIUnits.CELSIUS).convert(status.tmp.value).doubleValue()
252                     : status.tmp.value;
253         }
254         return status;
255     }
256
257     @Override
258     public void setAutoTimer(int index, String timerName, double value) throws ShellyApiException {
259         String type = SHELLY_CLASS_RELAY;
260         if (profile.isRoller) {
261             type = SHELLY_CLASS_ROLLER;
262         } else if (profile.isLight) {
263             type = SHELLY_CLASS_LIGHT;
264         }
265         String uri = SHELLY_URL_SETTINGS + "/" + type + "/" + index + "?" + timerName + "=" + value;
266         httpRequest(uri);
267     }
268
269     @Override
270     public void setSleepTime(int value) throws ShellyApiException {
271         httpRequest(SHELLY_URL_SETTINGS + "?sleep_time=" + value);
272     }
273
274     @Override
275     public void setValveTemperature(int valveId, int value) throws ShellyApiException {
276         httpRequest("/thermostat/" + valveId + "?target_t_enabled=1&target_t=" + value);
277     }
278
279     @Override
280     public void setValveMode(int valveId, boolean auto) throws ShellyApiException {
281         String uri = "/settings/thermostat/" + valveId + "?target_t_enabled=" + (auto ? "1" : "0");
282         if (auto && profile.settings.thermostats != null) {
283             uri = uri + "&target_t=" + getDouble(profile.settings.thermostats.get(0).targetTemp.value);
284         }
285         httpRequest(uri); // percentage to open the valve
286     }
287
288     @Override
289     public void setValveProfile(int valveId, int value) throws ShellyApiException {
290         String uri = "/settings/thermostat/" + valveId + "?";
291         httpRequest(uri + (value == 0 ? "schedule=0" : "schedule=1&schedule_profile=" + value));
292     }
293
294     @Override
295     public void setValvePosition(int valveId, double value) throws ShellyApiException {
296         httpRequest("/thermostat/" + valveId + "?pos=" + value); // percentage to open the valve
297     }
298
299     @Override
300     public void setValveBoostTime(int valveId, int value) throws ShellyApiException {
301         httpRequest("/settings/thermostat/" + valveId + "?boost_minutes=" + value);
302     }
303
304     @Override
305     public void startValveBoost(int valveId, int value) throws ShellyApiException {
306         if (profile.settings.thermostats != null) {
307             ShellyThermnostat t = profile.settings.thermostats.get(0);
308             int minutes = value != -1 ? value : getInteger(t.boostMinutes);
309             httpRequest("/thermostat/0?boost_minutes=" + minutes);
310         }
311     }
312
313     @Override
314     public void setLedStatus(String ledName, boolean value) throws ShellyApiException {
315         httpRequest(SHELLY_URL_SETTINGS + "?" + ledName + "=" + (value ? SHELLY_API_TRUE : SHELLY_API_FALSE));
316     }
317
318     public ShellySettingsLight getLightSettings() throws ShellyApiException {
319         return callApi(SHELLY_URL_SETTINGS_LIGHT, ShellySettingsLight.class);
320     }
321
322     @Override
323     public ShellyStatusLight getLightStatus() throws ShellyApiException {
324         return callApi(SHELLY_URL_STATUS, ShellyStatusLight.class);
325     }
326
327     public void setLightSetting(String parm, String value) throws ShellyApiException {
328         httpRequest(SHELLY_URL_SETTINGS + "?" + parm + "=" + value);
329     }
330
331     @Override
332     public ShellySettingsLogin getLoginSettings() throws ShellyApiException {
333         return callApi(SHELLY_URL_SETTINGS + "/login", ShellySettingsLogin.class);
334     }
335
336     @Override
337     public ShellySettingsLogin setLoginCredentials(String user, String password) throws ShellyApiException {
338         return callApi(SHELLY_URL_SETTINGS + "/login?enabled=yes&username=" + urlEncode(user) + "&password="
339                 + urlEncode(password), ShellySettingsLogin.class);
340     }
341
342     @Override
343     public String getCoIoTDescription() throws ShellyApiException {
344         try {
345             return callApi("/cit/d", String.class);
346         } catch (ShellyApiException e) {
347             if (e.getApiResult().isNotFound()) {
348                 return ""; // only supported by FW 1.10+
349             }
350             throw e;
351         }
352     }
353
354     @Override
355     public ShellySettingsLogin setCoIoTPeer(String peer) throws ShellyApiException {
356         return callApi(SHELLY_URL_SETTINGS + "?coiot_enable=true&coiot_peer=" + peer, ShellySettingsLogin.class);
357     }
358
359     @Override
360     public String deviceReboot() throws ShellyApiException {
361         return callApi(SHELLY_URL_RESTART, String.class);
362     }
363
364     @Override
365     public String factoryReset() throws ShellyApiException {
366         return callApi(SHELLY_URL_SETTINGS + "?reset=true", String.class);
367     }
368
369     @Override
370     public ShellyOtaCheckResult checkForUpdate() throws ShellyApiException {
371         return callApi("/ota/check", ShellyOtaCheckResult.class); // nw FW 1.10+: trigger update check
372     }
373
374     @Override
375     public String setWiFiRecovery(boolean enable) throws ShellyApiException {
376         return callApi(SHELLY_URL_SETTINGS + "?wifirecovery_reboot_enabled=" + (enable ? "true" : "false"),
377                 String.class); // FW 1.10+: Enable auto-restart on WiFi problems
378     }
379
380     @Override
381     public String setApRoaming(boolean enable) throws ShellyApiException { // FW 1.10+: Enable AP Roadming
382         return callApi(SHELLY_URL_SETTINGS + "?ap_roaming_enabled=" + (enable ? "true" : "false"), String.class);
383     }
384
385     @Override
386     public boolean setWiFiRangeExtender(boolean enable) throws ShellyApiException {
387         return false;
388     }
389
390     @Override
391     public boolean setEthernet(boolean enable) throws ShellyApiException {
392         return false;
393     }
394
395     @Override
396     public boolean setBluetooth(boolean enable) throws ShellyApiException {
397         return false;
398     }
399
400     @Override
401     public String resetStaCache() throws ShellyApiException { // FW 1.10+: Reset cached STA/AP list and to a rescan
402         return callApi("/sta_cache_reset", String.class);
403     }
404
405     @Override
406     public ShellySettingsUpdate firmwareUpdate(String uri) throws ShellyApiException {
407         return callApi("/ota?" + uri, ShellySettingsUpdate.class);
408     }
409
410     @Override
411     public String setCloud(boolean enabled) throws ShellyApiException {
412         return callApi("/settings/cloud/?enabled=" + (enabled ? "1" : "0"), String.class);
413     }
414
415     /**
416      * Change between White and Color Mode
417      *
418      * @param mode
419      * @throws ShellyApiException
420      */
421     @Override
422     public void setLightMode(String mode) throws ShellyApiException {
423         if (!mode.isEmpty() && !profile.device.mode.equals(mode)) {
424             setLightSetting(SHELLY_API_MODE, mode);
425             profile.device.mode = mode;
426             profile.inColor = profile.isLight && mode.equalsIgnoreCase(SHELLY_MODE_COLOR);
427         }
428     }
429
430     /**
431      * Set a single light parameter
432      *
433      * @param lightIndex Index of the light, usually 0 for Bulb and 0..3 for RGBW2.
434      * @param parm Name of the parameter (see API spec)
435      * @param value The value
436      * @throws ShellyApiException
437      */
438     @Override
439     public void setLightParm(int lightIndex, String parm, String value) throws ShellyApiException {
440         // Bulb, RGW2: /<color mode>/<light id>?parm?value
441         // Dimmer: /light/<light id>?parm=value
442         httpRequest(getControlUriPrefix(lightIndex) + "?" + parm + "=" + value);
443     }
444
445     @Override
446     public void setLightParms(int lightIndex, Map<String, String> parameters) throws ShellyApiException {
447         String url = getControlUriPrefix(lightIndex) + "?";
448         int i = 0;
449         for (String key : parameters.keySet()) {
450             if (i > 0) {
451                 url = url + "&";
452             }
453             url = url + key + "=" + parameters.get(key);
454             i++;
455         }
456         httpRequest(url);
457     }
458
459     /**
460      * Retrieve the IR Code list from the Shelly Sense device. The list could be customized by the user. It defines the
461      * symbolic key code, which gets
462      * map into a PRONTO code
463      *
464      * @return Map of key codes
465      * @throws ShellyApiException
466      */
467     public Map<String, String> getIRCodeList() throws ShellyApiException {
468         String result = httpRequest(SHELLY_URL_LIST_IR);
469         // take pragmatic approach to make the returned JSon into named arrays for Gson parsing
470         String keyList = substringAfter(result, "[");
471         keyList = substringBeforeLast(keyList, "]");
472         keyList = keyList.replaceAll(java.util.regex.Pattern.quote("\",\""), "\", \"name\": \"");
473         keyList = keyList.replaceAll(java.util.regex.Pattern.quote("["), "{ \"id\":");
474         keyList = keyList.replaceAll(java.util.regex.Pattern.quote("]"), "} ");
475         String json = "{\"key_codes\" : [" + keyList + "] }";
476         ShellySendKeyList codes = fromJson(gson, json, ShellySendKeyList.class);
477         Map<String, String> list = new HashMap<>();
478         for (ShellySenseKeyCode key : codes.keyCodes) {
479             if (key != null) {
480                 list.put(key.id, key.name);
481             }
482         }
483         return list;
484     }
485
486     /**
487      * Sends an IR key code to the Shelly Sense.
488      *
489      * @param keyCode A keyCoud could be a symbolic name (as defined in the key map on the device) or a PRONTO Code in
490      *            plain or hex64 format
491      *
492      * @throws ShellyApiException
493      * @throws IllegalArgumentException
494      */
495     @Override
496     public void sendIRKey(String keyCode) throws ShellyApiException, IllegalArgumentException {
497         String type = "";
498         if (profile.irCodes.containsKey(keyCode)) {
499             type = SHELLY_IR_CODET_STORED;
500         } else if ((keyCode.length() > 4) && keyCode.contains(" ")) {
501             type = SHELLY_IR_CODET_PRONTO;
502         } else {
503             type = SHELLY_IR_CODET_PRONTO_HEX;
504         }
505         String url = SHELLY_URL_SEND_IR + "?type=" + type;
506         if (type.equals(SHELLY_IR_CODET_STORED)) {
507             url = url + "&" + "id=" + keyCode;
508         } else if (type.equals(SHELLY_IR_CODET_PRONTO)) {
509             String code = Base64.getEncoder().encodeToString(keyCode.getBytes(StandardCharsets.UTF_8));
510             url = url + "&" + SHELLY_IR_CODET_PRONTO + "=" + code;
511         } else if (type.equals(SHELLY_IR_CODET_PRONTO_HEX)) {
512             url = url + "&" + SHELLY_IR_CODET_PRONTO_HEX + "=" + keyCode;
513         }
514         httpRequest(url);
515     }
516
517     public void setSenseSetting(String setting, String value) throws ShellyApiException {
518         httpRequest(SHELLY_URL_SETTINGS + "?" + setting + "=" + value);
519     }
520
521     /**
522      * Set event callback URLs. Depending on the device different event types are supported. In fact all of them will be
523      * redirected to the binding's servlet and act as a trigger to schedule a status update
524      *
525      * @throws ShellyApiException
526      */
527     @Override
528     public void setActionURLs() throws ShellyApiException {
529         setRelayEvents();
530         setDimmerEvents();
531         setSensorEventUrls();
532     }
533
534     private void setRelayEvents() throws ShellyApiException {
535         if (profile.settings.relays != null) {
536             int num = profile.isRoller ? profile.numRollers : profile.numRelays;
537             for (int i = 0; i < num; i++) {
538                 setEventUrls(i);
539             }
540         }
541     }
542
543     private void setDimmerEvents() throws ShellyApiException {
544         if (profile.settings.dimmers != null) {
545             int sz = profile.settings.dimmers.size();
546             for (int i = 0; i < sz; i++) {
547                 setEventUrls(i);
548             }
549         } else if (profile.isLight) {
550             setEventUrls(0);
551         }
552     }
553
554     @Override
555     public void muteSmokeAlarm(int id) throws ShellyApiException {
556         throw new ShellyApiException("Request not supported");
557     }
558
559     /**
560      * Set sensor Action URLs
561      *
562      * @throws ShellyApiException
563      */
564     private void setSensorEventUrls() throws ShellyApiException, ShellyApiException {
565         if (profile.isSensor) {
566             logger.debug("{}: Set Sensor Reporting URL", thingName);
567             setEventUrl(config.eventsSensorReport, SHELLY_EVENT_SENSORREPORT, SHELLY_EVENT_DARK, SHELLY_EVENT_TWILIGHT,
568                     SHELLY_EVENT_FLOOD_DETECTED, SHELLY_EVENT_FLOOD_GONE, SHELLY_EVENT_OPEN, SHELLY_EVENT_CLOSE,
569                     SHELLY_EVENT_VIBRATION, SHELLY_EVENT_ALARM_MILD, SHELLY_EVENT_ALARM_HEAVY, SHELLY_EVENT_ALARM_OFF,
570                     SHELLY_EVENT_TEMP_OVER, SHELLY_EVENT_TEMP_UNDER);
571         }
572     }
573
574     /**
575      * Set/delete Relay/Roller/Dimmer Action URLs
576      *
577      * @param index Device Index (0-based)
578      * @throws ShellyApiException
579      */
580     private void setEventUrls(Integer index) throws ShellyApiException {
581         if (profile.isRoller) {
582             setEventUrl(EVENT_TYPE_ROLLER, 0, config.eventsRoller, SHELLY_EVENT_ROLLER_OPEN, SHELLY_EVENT_ROLLER_CLOSE,
583                     SHELLY_EVENT_ROLLER_STOP);
584         } else if (profile.isDimmer) {
585             // 2 set of URLs
586             setEventUrl(EVENT_TYPE_LIGHT, index, config.eventsButton, SHELLY_EVENT_BTN1_ON, SHELLY_EVENT_BTN1_OFF,
587                     SHELLY_EVENT_BTN2_ON, SHELLY_EVENT_BTN2_OFF);
588             setEventUrl(EVENT_TYPE_LIGHT, index, config.eventsPush, SHELLY_EVENT_SHORTPUSH1, SHELLY_EVENT_LONGPUSH1,
589                     SHELLY_EVENT_SHORTPUSH2, SHELLY_EVENT_LONGPUSH2);
590
591             // Relay output
592             setEventUrl(EVENT_TYPE_LIGHT, index, config.eventsSwitch, SHELLY_EVENT_OUT_ON, SHELLY_EVENT_OUT_OFF);
593         } else if (profile.hasRelays) {
594             // Standard relays: btn_xxx, out_xxx, short/longpush URLs
595             setEventUrl(EVENT_TYPE_RELAY, index, config.eventsButton, SHELLY_EVENT_BTN_ON, SHELLY_EVENT_BTN_OFF);
596             setEventUrl(EVENT_TYPE_RELAY, index, config.eventsPush, SHELLY_EVENT_SHORTPUSH, SHELLY_EVENT_LONGPUSH);
597             setEventUrl(EVENT_TYPE_RELAY, index, config.eventsSwitch, SHELLY_EVENT_OUT_ON, SHELLY_EVENT_OUT_OFF);
598         } else if (profile.isLight) {
599             // Duo, Bulb
600             setEventUrl(EVENT_TYPE_LIGHT, index, config.eventsSwitch, SHELLY_EVENT_OUT_ON, SHELLY_EVENT_OUT_OFF);
601         }
602     }
603
604     private void setEventUrl(boolean enabled, String... eventTypes) throws ShellyApiException {
605         if (config.localIp.isEmpty()) {
606             throw new ShellyApiException(thingName + ": Local IP address was not detected, can't build Callback URL");
607         }
608         for (String eventType : eventTypes) {
609             if (profile.containsEventUrl(eventType)) {
610                 // H&T adds the type=xx to report_url itself, so we need to ommit here
611                 String eclass = profile.isSensor ? EVENT_TYPE_SENSORDATA : eventType;
612                 String urlParm = eventType.contains("temp") || profile.isHT ? "" : "?type=" + eventType;
613                 String callBackUrl = "http://" + config.localIp + ":" + config.localPort + SHELLY1_CALLBACK_URI + "/"
614                         + profile.thingName + "/" + eclass + urlParm;
615                 String newUrl = enabled ? callBackUrl : SHELLY_NULL_URL;
616                 String testUrl = "\"" + mkEventUrl(eventType) + "\":\"" + newUrl + "\"";
617                 if (!enabled && !profile.settingsJson.contains(testUrl)) {
618                     // Don't set URL to null when the current one doesn't point to this OH
619                     // Don't interfere with a 3rd party App
620                     continue;
621                 }
622                 if (!profile.settingsJson.contains(testUrl)) {
623                     // Current Action URL is != new URL
624                     logger.debug("{}: Set new url for event type {}: {}", thingName, eventType, newUrl);
625                     httpRequest(SHELLY_URL_SETTINGS + "?" + mkEventUrl(eventType) + "=" + urlEncode(newUrl));
626                 }
627             }
628         }
629     }
630
631     private void setEventUrl(String deviceClass, Integer index, boolean enabled, String... eventTypes)
632             throws ShellyApiException {
633         for (String eventType : eventTypes) {
634             if (profile.containsEventUrl(eventType)) {
635                 String callBackUrl = "http://" + config.localIp + ":" + config.localPort + SHELLY1_CALLBACK_URI + "/"
636                         + profile.thingName + "/" + deviceClass + "/" + index + "?type=" + eventType;
637                 String newUrl = enabled ? callBackUrl : SHELLY_NULL_URL;
638                 String test = "\"" + mkEventUrl(eventType) + "\":\"" + callBackUrl + "\"";
639                 if (!enabled && !profile.settingsJson.contains(test)) {
640                     // Don't set URL to null when the current one doesn't point to this OH
641                     // Don't interfere with a 3rd party App
642                     continue;
643                 }
644                 test = "\"" + mkEventUrl(eventType) + "\":\"" + newUrl + "\"";
645                 if (!profile.settingsJson.contains(test)) {
646                     // Current Action URL is != new URL
647                     logger.debug("{}: Set URL for type {} to {}", thingName, eventType, newUrl);
648                     httpRequest(SHELLY_URL_SETTINGS + "/" + deviceClass + "/" + index + "?" + mkEventUrl(eventType)
649                             + "=" + urlEncode(newUrl));
650                 }
651             }
652         }
653     }
654
655     private static String mkEventUrl(String eventType) {
656         return eventType + SHELLY_EVENTURL_SUFFIX;
657     }
658
659     @Override
660     public String getControlUriPrefix(Integer id) {
661         String uri = "";
662         if (profile.isLight || profile.isDimmer) {
663             if (profile.isDuo || profile.isDimmer) {
664                 // Duo + Dimmer
665                 uri = SHELLY_URL_CONTROL_LIGHT;
666             } else {
667                 // Bulb + RGBW2
668                 uri = "/" + (profile.inColor ? SHELLY_MODE_COLOR : SHELLY_MODE_WHITE);
669             }
670         } else {
671             // Roller, Relay
672             uri = SHELLY_URL_CONTROL_RELEAY;
673         }
674         uri = uri + "/" + id;
675         return uri;
676     }
677
678     @Override
679     public int getTimeoutErrors() {
680         return timeoutErrors;
681     }
682
683     @Override
684     public int getTimeoutsRecovered() {
685         return timeoutsRecovered;
686     }
687
688     @Override
689     public void close() {
690     }
691
692     @Override
693     public void startScan() {
694     }
695 }