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