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