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