]> git.basschouten.com Git - openhab-addons.git/blob
6230bbeb03532c35366ae9d681d7acc57bfe0261
[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.api;
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.discovery.ShellyThingCreator.*;
18 import static org.openhab.binding.shelly.internal.util.ShellyUtils.*;
19
20 import java.util.HashMap;
21 import java.util.Map;
22 import java.util.regex.Matcher;
23 import java.util.regex.Pattern;
24
25 import org.eclipse.jdt.annotation.NonNullByDefault;
26 import org.eclipse.jdt.annotation.Nullable;
27 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsDimmer;
28 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsGlobal;
29 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsInput;
30 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsRelay;
31 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsRgbwLight;
32 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsStatus;
33 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyThermnostat;
34 import org.openhab.binding.shelly.internal.util.ShellyVersionDTO;
35 import org.slf4j.Logger;
36 import org.slf4j.LoggerFactory;
37
38 import com.google.gson.Gson;
39
40 /**
41  * The {@link ShellyDeviceProfile} creates a device profile based on the settings returned from the API's /settings
42  * call. This is used to be more dynamic in controlling the device, but also to overcome some issues in the API (e.g.
43  * RGBW2 returns "no meter" even it has one)
44  *
45  * @author Markus Michels - Initial contribution
46  */
47 @NonNullByDefault
48 public class ShellyDeviceProfile {
49     private final Logger logger = LoggerFactory.getLogger(ShellyDeviceProfile.class);
50     private static final Pattern VERSION_PATTERN = Pattern.compile("v\\d+\\.\\d+\\.\\d+(-[a-z0-9]*)?");
51
52     public boolean initialized = false; // true when initialized
53
54     public String thingName = "";
55     public String deviceType = "";
56     public boolean extFeatures = false;
57
58     public String settingsJson = "";
59     public ShellySettingsGlobal settings = new ShellySettingsGlobal();
60     public ShellySettingsStatus status = new ShellySettingsStatus();
61
62     public String hostname = "";
63     public String name = "";
64     public String model = "";
65     public String mode = "";
66     public boolean discoverable = true;
67     public boolean auth = false;
68     public boolean alwaysOn = true;
69     public boolean isGen2 = false;
70     public boolean isBlu = false;
71     public String gateway = "";
72
73     public String hwRev = "";
74     public String hwBatchId = "";
75     public String mac = "";
76     public String fwVersion = "";
77     public String fwDate = "";
78
79     public boolean hasRelays = false; // true if it has at least 1 power meter
80     public int numRelays = 0; // number of relays/outputs
81     public int numRollers = 0; // number of Rollers, usually 1
82     public boolean isRoller = false; // true for Shelly2 in roller mode
83     public boolean isDimmer = false; // true for a Shelly Dimmer (SHDM-1)
84     public int numInputs = 0; // number of inputs
85
86     public int numMeters = 0;
87     public boolean isEMeter = false; // true for ShellyEM/3EM
88
89     public boolean isLight = false; // true if it is a Shelly Bulb/RGBW2
90     public boolean isBulb = false; // true only if it is a Bulb
91     public boolean isDuo = false; // true only if it is a Duo
92     public boolean isRGBW2 = false; // true only if it a RGBW2
93     public boolean inColor = false; // true if bulb/rgbw2 is in color mode
94
95     public boolean isSensor = false; // true for HT & Smoke
96     public boolean hasBattery = false; // true if battery device
97     public boolean isSense = false; // true if thing is a Shelly Sense
98     public boolean isMotion = false; // true if thing is a Shelly Sense
99     public boolean isHT = false; // true for H&T
100     public boolean isDW = false; // true for Door Window sensor
101     public boolean isButton = false; // true for a Shelly Button 1
102     public boolean isIX = false; // true for a Shelly IX
103     public boolean isTRV = false; // true for a Shelly TRV
104     public boolean isSmoke = false; // true for Shelly Smoke
105
106     public int minTemp = 0; // Bulb/Duo: Min Light Temp
107     public int maxTemp = 0; // Bulb/Duo: Max Light Temp
108
109     public int updatePeriod = 2 * UPDATE_SETTINGS_INTERVAL_SECONDS + 10;
110
111     public String coiotEndpoint = "";
112
113     public Map<String, String> irCodes = new HashMap<>(); // Sense: list of stored IR codes
114
115     public ShellyDeviceProfile() {
116     }
117
118     public ShellyDeviceProfile initialize(String thingType, String jsonIn) throws ShellyApiException {
119         Gson gson = new Gson();
120
121         initialized = false;
122
123         initFromThingType(thingType);
124
125         String json = jsonIn;
126         if (json.contains("\"ext_temperature\":{\"0\":[{")) {
127             // Shelly UNI uses ext_temperature array, reformat to avoid GSON exception
128             json = json.replace("ext_temperature", "ext_temperature_array");
129         }
130         if (json.contains("\"ext_humidity\":{\"0\":[{")) {
131             // Shelly UNI uses ext_humidity array, reformat to avoid GSON exception
132             json = json.replace("ext_humidity", "ext_humidity_array");
133         }
134         settingsJson = json;
135         settings = fromJson(gson, json, ShellySettingsGlobal.class);
136
137         // General settings
138         name = getString(settings.name);
139         deviceType = getString(settings.device.type);
140         mac = getString(settings.device.mac);
141         hostname = settings.device.hostname != null && !settings.device.hostname.isEmpty()
142                 ? settings.device.hostname.toLowerCase()
143                 : "shelly-" + mac.toUpperCase().substring(6, 11);
144         mode = getString(settings.mode).toLowerCase();
145         hwRev = settings.hwinfo != null ? getString(settings.hwinfo.hwRevision) : "";
146         hwBatchId = settings.hwinfo != null ? getString(settings.hwinfo.batchId.toString()) : "";
147         fwDate = substringBefore(settings.fw, "/");
148         fwVersion = extractFwVersion(settings.fw);
149         ShellyVersionDTO version = new ShellyVersionDTO();
150         extFeatures = version.compare(fwVersion, SHELLY_API_FW_110) >= 0;
151         discoverable = (settings.discoverable == null) || settings.discoverable;
152
153         isRoller = mode.equalsIgnoreCase(SHELLY_MODE_ROLLER);
154         inColor = isLight && mode.equalsIgnoreCase(SHELLY_MODE_COLOR);
155
156         numRelays = !isLight ? getInteger(settings.device.numOutputs) : 0;
157         if ((numRelays > 0) && (settings.relays == null)) {
158             numRelays = 0;
159         }
160         hasRelays = (numRelays > 0) || isDimmer;
161         numRollers = getInteger(settings.device.numRollers);
162         numInputs = settings.inputs != null ? settings.inputs.size() : hasRelays ? isRoller ? 2 : 1 : 0;
163
164         isEMeter = settings.emeters != null;
165         numMeters = !isEMeter ? getInteger(settings.device.numMeters) : getInteger(settings.device.numEMeters);
166         if ((numMeters == 0) && isLight) {
167             // RGBW2 doesn't report, but has one
168             numMeters = inColor ? 1 : getInteger(settings.device.numOutputs);
169         }
170
171         initialized = true;
172         return this;
173     }
174
175     public boolean containsEventUrl(String eventType) {
176         return containsEventUrl(settingsJson, eventType);
177     }
178
179     public boolean containsEventUrl(String json, String eventType) {
180         String settings = json.toLowerCase();
181         return settings.contains((eventType + SHELLY_EVENTURL_SUFFIX).toLowerCase());
182     }
183
184     public boolean isInitialized() {
185         return initialized;
186     }
187
188     public void initFromThingType(String name) {
189         String thingType = (name.contains("-") ? substringBefore(name, "-") : name).toLowerCase().trim();
190         if (thingType.isEmpty()) {
191             return;
192         }
193
194         isBlu = thingType.startsWith("shellyblu"); // e.g. SBBT for BU Button
195
196         isDimmer = deviceType.equalsIgnoreCase(SHELLYDT_DIMMER) || deviceType.equalsIgnoreCase(SHELLYDT_DIMMER2);
197         isBulb = thingType.equals(THING_TYPE_SHELLYBULB_STR);
198         isDuo = thingType.equals(THING_TYPE_SHELLYDUO_STR) || thingType.equals(THING_TYPE_SHELLYVINTAGE_STR)
199                 || thingType.equals(THING_TYPE_SHELLYDUORGBW_STR);
200         isRGBW2 = thingType.startsWith(THING_TYPE_SHELLYRGBW2_PREFIX);
201         isLight = isBulb || isDuo || isRGBW2;
202         if (isLight) {
203             minTemp = isBulb ? MIN_COLOR_TEMP_BULB : MIN_COLOR_TEMP_DUO;
204             maxTemp = isBulb ? MAX_COLOR_TEMP_BULB : MAX_COLOR_TEMP_DUO;
205         }
206
207         boolean isFlood = thingType.equals(THING_TYPE_SHELLYFLOOD_STR);
208         isSmoke = thingType.equals(THING_TYPE_SHELLYSMOKE_STR) || thingType.equals(THING_TYPE_SHELLYPLUSSMOKE_STR);
209         boolean isGas = thingType.equals(THING_TYPE_SHELLYGAS_STR);
210         boolean isUNI = thingType.equals(THING_TYPE_SHELLYUNI_STR);
211         isHT = thingType.equals(THING_TYPE_SHELLYHT_STR) || thingType.equals(THING_TYPE_SHELLYPLUSHT_STR);
212         isDW = thingType.equals(THING_TYPE_SHELLYDOORWIN_STR) || thingType.equals(THING_TYPE_SHELLYDOORWIN2_STR)
213                 || thingType.equals(THING_TYPE_SHELLYBLUDW_STR);
214         isMotion = thingType.startsWith(THING_TYPE_SHELLYMOTION_STR);
215         isSense = thingType.equals(THING_TYPE_SHELLYSENSE_STR);
216         isIX = thingType.equals(THING_TYPE_SHELLYIX3_STR) || thingType.equals(THING_TYPE_SHELLYPLUSI4_STR)
217                 || thingType.equals(THING_TYPE_SHELLYPLUSI4DC_STR);
218         isButton = thingType.equals(THING_TYPE_SHELLYBUTTON1_STR) || thingType.equals(THING_TYPE_SHELLYBUTTON2_STR)
219                 || thingType.equals(THING_TYPE_SHELLYBLUBUTTON_STR);
220         isSensor = isHT || isFlood || isDW || isSmoke || isGas || isButton || isUNI || isMotion || isSense || isTRV;
221         hasBattery = isHT || isFlood || isDW || isSmoke || isButton || isMotion || isTRV;
222         isTRV = thingType.equals(THING_TYPE_SHELLYTRV_STR);
223
224         alwaysOn = !hasBattery || isMotion || isSense; // true means: device is reachable all the time (no sleep mode)
225     }
226
227     public void updateFromStatus(ShellySettingsStatus status) {
228         if (hasRelays) {
229             // Dimmer-2 doesn't report inputs under /settings, only on /status, we need to update that info after init
230             if (status.inputs != null) {
231                 numInputs = status.inputs.size();
232             }
233         } else if (status.input != null) {
234             // RGBW2
235             numInputs = 1;
236         }
237     }
238
239     public String getControlGroup(int i) {
240         if (i < 0) {
241             logger.debug("{}: Invalid index {} for getControlGroup()", thingName, i);
242             return "";
243         }
244         int idx = i + 1;
245         if (isDimmer) {
246             return CHANNEL_GROUP_DIMMER_CONTROL;
247         } else if (isRoller) {
248             return numRollers <= 1 ? CHANNEL_GROUP_ROL_CONTROL : CHANNEL_GROUP_ROL_CONTROL + idx;
249         } else if (isDimmer) {
250             return CHANNEL_GROUP_RELAY_CONTROL;
251         } else if (hasRelays) {
252             return numRelays <= 1 ? CHANNEL_GROUP_RELAY_CONTROL : CHANNEL_GROUP_RELAY_CONTROL + idx;
253         } else if (isRGBW2) {
254             return settings.lights == null || settings.lights != null && settings.lights.size() <= 1
255                     ? CHANNEL_GROUP_LIGHT_CONTROL
256                     : CHANNEL_GROUP_LIGHT_CHANNEL + idx;
257         } else if (isLight) {
258             return CHANNEL_GROUP_LIGHT_CONTROL;
259         } else if (isButton) {
260             return CHANNEL_GROUP_STATUS;
261         } else if (isSensor) {
262             return CHANNEL_GROUP_SENSOR;
263         }
264
265         // e.g. ix3
266         return numRelays == 1 ? CHANNEL_GROUP_STATUS : CHANNEL_GROUP_STATUS + idx;
267     }
268
269     public String getMeterGroup(int idx) {
270         return numMeters > 1 ? CHANNEL_GROUP_METER + (idx + 1) : CHANNEL_GROUP_METER;
271     }
272
273     public String getInputGroup(int i) {
274         int idx = i + 1; // group names are 1-based
275         if (isRGBW2) {
276             return CHANNEL_GROUP_LIGHT_CONTROL;
277         } else if (isIX) {
278             return CHANNEL_GROUP_STATUS + idx;
279         } else if (isButton) {
280             return CHANNEL_GROUP_STATUS;
281         } else if (isRoller) {
282             return numRelays <= 2 ? CHANNEL_GROUP_ROL_CONTROL : CHANNEL_GROUP_ROL_CONTROL + idx;
283         } else {
284             // Device has 1 input per relay: 0=off, 1+2 depend on switch mode
285             return numRelays <= 1 ? CHANNEL_GROUP_RELAY_CONTROL : CHANNEL_GROUP_RELAY_CONTROL + idx;
286         }
287     }
288
289     public String getInputSuffix(int i) {
290         int idx = i + 1; // channel names are 1-based
291         if (isRGBW2 || isIX) {
292             return ""; // RGBW2 has only 1 channel
293         } else if (isRoller || isDimmer) {
294             // Roller has 2 relays, but it will be mapped to 1 roller with 2 inputs
295             return String.valueOf(idx);
296         } else if (hasRelays) {
297             return numRelays == 1 && numInputs >= 2 ? String.valueOf(idx) : "";
298         }
299         return "";
300     }
301
302     @SuppressWarnings("null")
303     public boolean inButtonMode(int idx) {
304         if (idx < 0) {
305             logger.debug("{}: Invalid index {} for inButtonMode()", thingName, idx);
306             return false;
307         }
308         String btnType = "";
309         if (isButton) {
310             return true;
311         } else if (isIX && settings.inputs != null && idx < settings.inputs.size()) {
312             ShellySettingsInput input = settings.inputs.get(idx);
313             btnType = getString(input.btnType);
314         } else if (isDimmer) {
315             if (settings.dimmers != null) {
316                 ShellySettingsDimmer dimmer = settings.dimmers.get(0);
317                 btnType = dimmer.btnType;
318             }
319         } else if (settings.relays != null) {
320             if (numRelays == 1) {
321                 ShellySettingsRelay relay = settings.relays.get(0);
322                 if (relay.btnType != null) {
323                     btnType = getString(relay.btnType);
324                 } else {
325                     // Shelly 1L has 2 inputs
326                     btnType = idx == 0 ? getString(relay.btnType1) : getString(relay.btnType2);
327                 }
328             } else if (idx < settings.relays.size()) {
329                 // only one input channel
330                 ShellySettingsRelay relay = settings.relays.get(idx);
331                 btnType = getString(relay.btnType);
332             }
333         } else if (isRGBW2 && (settings.lights != null) && (idx < settings.lights.size())) {
334             ShellySettingsRgbwLight light = settings.lights.get(idx);
335             btnType = light.btnType;
336         }
337
338         return btnType.equalsIgnoreCase(SHELLY_BTNT_MOMENTARY) || btnType.equalsIgnoreCase(SHELLY_BTNT_MOM_ON_RELEASE)
339                 || btnType.equalsIgnoreCase(SHELLY_BTNT_ONE_BUTTON) || btnType.equalsIgnoreCase(SHELLY_BTNT_TWO_BUTTON)
340                 || btnType.equalsIgnoreCase(SHELLY_BTNT_DETACHED);
341     }
342
343     public int getRollerFav(int id) {
344         if (id >= 0 && getBool(settings.favoritesEnabled) && settings.favorites != null
345                 && id < settings.favorites.size()) {
346             return settings.favorites.get(id).pos;
347         }
348         return -1;
349     }
350
351     public String[] getValveProfileList(int valveId) {
352         if (isTRV && settings.thermostats != null) {
353             int sz = settings.thermostats.size();
354             if (valveId <= sz) {
355                 if (settings.thermostats != null) {
356                     ShellyThermnostat t = settings.thermostats.get(valveId);
357                     return t.profileNames;
358                 }
359             }
360         }
361         return new String[0];
362     }
363
364     public String getValueProfile(int valveId, int profileId) {
365         int id = profileId;
366         if (id <= 0 && settings.thermostats != null) {
367             id = settings.thermostats.get(0).profile;
368         }
369         return "" + id;
370     }
371
372     public static String extractFwVersion(@Nullable String version) {
373         if (version != null) {
374             // fix version e.g.
375             // 20210319-122304/v.1.10-Dimmer1-gfd4cc10 (with v.1. instead of v1.)
376             // 20220809-125346/v1.12-g99f7e0b (.0 in 1.12.0 missing)
377             String vers = version.replace("/v.1.10-", "/v1.10.0-") //
378                     .replace("/v1.12-", "/v1.12.0");
379
380             // Extract version from string, e.g. 20210226-091047/v1.10.0-rc2-89-g623b41ec0-master
381             Matcher matcher = VERSION_PATTERN.matcher(vers);
382             if (matcher.find()) {
383                 return matcher.group(0);
384             }
385         }
386         return "";
387     }
388
389     public boolean coiotEnabled() {
390         if ((settings.coiot != null) && (settings.coiot.enabled != null)) {
391             return settings.coiot.enabled;
392         }
393
394         // If device is not yet intialized or the enabled property is missing we assume that CoIoT is enabled
395         return true;
396     }
397 }