2 * Copyright (c) 2010-2021 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.shelly.internal.api;
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.*;
19 import java.util.HashMap;
21 import java.util.regex.Matcher;
22 import java.util.regex.Pattern;
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.slf4j.Logger;
33 import org.slf4j.LoggerFactory;
35 import com.google.gson.Gson;
38 * The {@link ShellyDeviceProfile} creates a device profile based on the settings returned from the API's /settings
39 * call. This is used to be more dynamic in controlling the device, but also to overcome some issues in the API (e.g.
40 * RGBW2 returns "no meter" even it has one)
42 * @author Markus Michels - Initial contribution
45 public class ShellyDeviceProfile {
46 private final Logger logger = LoggerFactory.getLogger(ShellyDeviceProfile.class);
47 private final static Pattern VERSION_PATTERN = Pattern.compile("v\\d+\\.\\d+\\.\\d+");
49 public boolean initialized = false; // true when initialized
51 public String thingName = "";
52 public String deviceType = "";
54 public String settingsJson = "";
55 public ShellySettingsGlobal settings = new ShellySettingsGlobal();
56 public ShellySettingsStatus status = new ShellySettingsStatus();
58 public String hostname = "";
59 public String mode = "";
60 public boolean discoverable = true;
61 public boolean auth = false;
62 public boolean alwaysOn = true;
64 public String hwRev = "";
65 public String hwBatchId = "";
66 public String mac = "";
67 public String fwId = "";
68 public String fwVersion = "";
69 public String fwDate = "";
71 public boolean hasRelays = false; // true if it has at least 1 power meter
72 public int numRelays = 0; // number of relays/outputs
73 public int numRollers = 0; // number of Rollers, usually 1
74 public boolean isRoller = false; // true for Shelly2 in roller mode
75 public boolean isDimmer = false; // true for a Shelly Dimmer (SHDM-1)
76 public int numInputs = 0; // number of inputs
78 public int numMeters = 0;
79 public boolean isEMeter = false; // true for ShellyEM/3EM
81 public boolean isLight = false; // true if it is a Shelly Bulb/RGBW2
82 public boolean isBulb = false; // true only if it is a Bulb
83 public boolean isDuo = false; // true only if it is a Duo
84 public boolean isRGBW2 = false; // true only if it a a RGBW2
85 public boolean inColor = false; // true if bulb/rgbw2 is in color mode
87 public boolean isSensor = false; // true for HT & Smoke
88 public boolean hasBattery = false; // true if battery device
89 public boolean isSense = false; // true if thing is a Shelly Sense
90 public boolean isMotion = false; // true if thing is a Shelly Sense
91 public boolean isHT = false; // true for H&T
92 public boolean isDW = false; // true for Door Window sensor
93 public boolean isButton = false; // true for a Shelly Button 1
94 public boolean isIX3 = false; // true for a Shelly IX
96 public int minTemp = 0; // Bulb/Duo: Min Light Temp
97 public int maxTemp = 0; // Bulb/Duo: Max Light Temp
99 public int updatePeriod = 2 * UPDATE_SETTINGS_INTERVAL_SECONDS + 10;
101 public String coiotEndpoint = "";
103 public Map<String, String> irCodes = new HashMap<>(); // Sense: list of stored IR codes
105 public ShellyDeviceProfile() {
108 public ShellyDeviceProfile initialize(String thingType, String json) throws ShellyApiException {
109 Gson gson = new Gson();
113 initFromThingType(thingType);
115 ShellySettingsGlobal gs = fromJson(gson, json, ShellySettingsGlobal.class);
116 settings = gs; // only update when no exception
119 deviceType = getString(settings.device.type);
120 mac = getString(settings.device.mac);
121 hostname = settings.device.hostname != null && !settings.device.hostname.isEmpty()
122 ? settings.device.hostname.toLowerCase()
123 : "shelly-" + mac.toUpperCase().substring(6, 11);
124 mode = getString(settings.mode).toLowerCase();
125 hwRev = settings.hwinfo != null ? getString(settings.hwinfo.hwRevision) : "";
126 hwBatchId = settings.hwinfo != null ? getString(settings.hwinfo.batchId.toString()) : "";
127 fwDate = substringBefore(settings.fw, "/");
128 fwVersion = extractFwVersion(settings.fw);
129 fwId = substringAfter(settings.fw, "@");
130 discoverable = (settings.discoverable == null) || settings.discoverable;
132 inColor = isLight && mode.equalsIgnoreCase(SHELLY_MODE_COLOR);
134 numRelays = !isLight ? getInteger(settings.device.numOutputs) : 0;
135 if ((numRelays > 0) && (settings.relays == null)) {
138 hasRelays = (numRelays > 0) || isDimmer;
139 numRollers = getInteger(settings.device.numRollers);
140 numInputs = settings.inputs != null ? settings.inputs.size() : hasRelays ? isRoller ? 2 : 1 : 0;
142 isEMeter = settings.emeters != null;
143 numMeters = !isEMeter ? getInteger(settings.device.numMeters) : getInteger(settings.device.numEMeters);
144 if ((numMeters == 0) && isLight) {
145 // RGBW2 doesn't report, but has one
146 numMeters = inColor ? 1 : getInteger(settings.device.numOutputs);
149 if (settings.sleepMode != null) {
150 // Sensor, usually 12h, H&T in USB mode 10min
151 updatePeriod = getString(settings.sleepMode.unit).equalsIgnoreCase("m") ? settings.sleepMode.period * 60 // minutes
152 : settings.sleepMode.period * 3600; // hours
153 updatePeriod += 60; // give 1min extra
154 } else if ((settings.coiot != null) && (settings.coiot.updatePeriod != null)) {
155 // Derive from CoAP update interval, usually 2*15+10s=40sec -> 70sec
156 updatePeriod = Math.max(UPDATE_SETTINGS_INTERVAL_SECONDS, 2 * getInteger(settings.coiot.updatePeriod)) + 10;
158 updatePeriod = UPDATE_SETTINGS_INTERVAL_SECONDS + 10;
165 public boolean containsEventUrl(String eventType) {
166 return containsEventUrl(settingsJson, eventType);
169 public boolean containsEventUrl(String json, String eventType) {
170 String settings = json.toLowerCase();
171 return settings.contains((eventType + SHELLY_EVENTURL_SUFFIX).toLowerCase());
174 public boolean isInitialized() {
178 public void initFromThingType(String name) {
179 String thingType = (name.contains("-") ? substringBefore(name, "-") : name).toLowerCase().trim();
180 if (thingType.isEmpty()) {
184 isDimmer = deviceType.equalsIgnoreCase(SHELLYDT_DIMMER) || deviceType.equalsIgnoreCase(SHELLYDT_DIMMER2);
185 isRoller = mode.equalsIgnoreCase(SHELLY_MODE_ROLLER);
187 isBulb = thingType.equals(THING_TYPE_SHELLYBULB_STR);
188 isDuo = thingType.equals(THING_TYPE_SHELLYDUO_STR) || thingType.equals(THING_TYPE_SHELLYVINTAGE_STR)
189 || thingType.equals(THING_TYPE_SHELLYDUORGBW_STR);
190 isRGBW2 = thingType.startsWith(THING_TYPE_SHELLYRGBW2_PREFIX);
191 isLight = isBulb || isDuo || isRGBW2;
193 minTemp = isBulb ? MIN_COLOR_TEMP_BULB : MIN_COLOR_TEMP_DUO;
194 maxTemp = isBulb ? MAX_COLOR_TEMP_BULB : MAX_COLOR_TEMP_DUO;
197 boolean isFlood = thingType.equals(THING_TYPE_SHELLYFLOOD_STR);
198 boolean isSmoke = thingType.equals(THING_TYPE_SHELLYSMOKE_STR);
199 boolean isGas = thingType.equals(THING_TYPE_SHELLYGAS_STR);
200 boolean isUNI = thingType.equals(THING_TYPE_SHELLYUNI_STR);
201 isHT = thingType.equals(THING_TYPE_SHELLYHT_STR);
202 isDW = thingType.equals(THING_TYPE_SHELLYDOORWIN_STR) || thingType.equals(THING_TYPE_SHELLYDOORWIN2_STR);
203 isMotion = thingType.startsWith(THING_TYPE_SHELLYMOTION_STR);
204 isSense = thingType.equals(THING_TYPE_SHELLYSENSE_STR);
205 isIX3 = thingType.equals(THING_TYPE_SHELLYIX3_STR);
206 isButton = thingType.equals(THING_TYPE_SHELLYBUTTON1_STR);
207 isSensor = isHT || isFlood || isDW || isSmoke || isGas || isButton || isUNI || isMotion || isSense;
208 hasBattery = isHT || isFlood || isDW || isSmoke || isButton || isMotion;
210 alwaysOn = !hasBattery || isMotion || isSense; // true means: device is reachable all the time (no sleep mode)
213 public void updateFromStatus(ShellySettingsStatus status) {
215 // Dimmer-2 doesn't report inputs under /settings, only on /status, we need to update that info after init
216 if (status.inputs != null) {
217 numInputs = status.inputs.size();
219 } else if (status.input != null) {
225 public String getControlGroup(int i) {
227 logger.debug("{}: Invalid index {} for getControlGroup()", thingName, i);
232 return CHANNEL_GROUP_DIMMER_CONTROL;
233 } else if (isRoller) {
234 return numRollers <= 1 ? CHANNEL_GROUP_ROL_CONTROL : CHANNEL_GROUP_ROL_CONTROL + idx;
235 } else if (isDimmer) {
236 return CHANNEL_GROUP_RELAY_CONTROL;
237 } else if (hasRelays) {
238 return numRelays <= 1 ? CHANNEL_GROUP_RELAY_CONTROL : CHANNEL_GROUP_RELAY_CONTROL + idx;
239 } else if (isLight) {
240 return numRelays <= 1 ? CHANNEL_GROUP_LIGHT_CONTROL : CHANNEL_GROUP_LIGHT_CONTROL + idx;
241 } else if (isButton) {
242 return CHANNEL_GROUP_STATUS;
243 } else if (isSensor) {
244 return CHANNEL_GROUP_SENSOR;
248 return numRelays == 1 ? CHANNEL_GROUP_STATUS : CHANNEL_GROUP_STATUS + idx;
251 public String getInputGroup(int i) {
252 int idx = i + 1; // group names are 1-based
254 return CHANNEL_GROUP_LIGHT_CONTROL;
256 return CHANNEL_GROUP_STATUS + idx;
257 } else if (isButton) {
258 return CHANNEL_GROUP_STATUS;
259 } else if (isRoller) {
260 return numRelays <= 2 ? CHANNEL_GROUP_ROL_CONTROL : CHANNEL_GROUP_ROL_CONTROL + idx;
262 // Device has 1 input per relay: 0=off, 1+2 depend on switch mode
263 return numRelays <= 1 ? CHANNEL_GROUP_RELAY_CONTROL : CHANNEL_GROUP_RELAY_CONTROL + idx;
267 public String getInputSuffix(int i) {
268 int idx = i + 1; // channel names are 1-based
269 if (isRGBW2 || isIX3) {
270 return ""; // RGBW2 has only 1 channel
271 } else if (isRoller || isDimmer) {
272 // Roller has 2 relays, but it will be mapped to 1 roller with 2 inputs
273 return String.valueOf(idx);
274 } else if (hasRelays) {
275 return (numRelays) == 1 && (numInputs >= 2) ? String.valueOf(idx) : "";
280 public boolean inButtonMode(int idx) {
282 logger.debug("{}: Invalid index {} for inButtonMode()", thingName, idx);
288 } else if (isIX3 && (settings.inputs != null) && (idx < settings.inputs.size())) {
289 ShellySettingsInput input = settings.inputs.get(idx);
290 btnType = getString(input.btnType);
291 } else if (isDimmer) {
292 if (settings.dimmers != null) {
293 ShellySettingsDimmer dimmer = settings.dimmers.get(0);
294 btnType = dimmer.btnType;
296 } else if (settings.relays != null) {
297 if (numRelays == 1) {
298 ShellySettingsRelay relay = settings.relays.get(0);
299 if (relay.btnType != null) {
300 btnType = getString(relay.btnType);
302 // Shelly 1L has 2 inputs
303 btnType = idx == 0 ? getString(relay.btnType1) : getString(relay.btnType2);
305 } else if (idx < settings.relays.size()) {
306 // only one input channel
307 ShellySettingsRelay relay = settings.relays.get(idx);
308 btnType = getString(relay.btnType);
310 } else if (isRGBW2 && (settings.lights != null) && (idx < settings.lights.size())) {
311 ShellySettingsRgbwLight light = settings.lights.get(idx);
312 btnType = light.btnType;
315 logger.trace("{}: Checking for trigger, button-type[{}] is {}", thingName, idx, btnType);
316 return btnType.equalsIgnoreCase(SHELLY_BTNT_MOMENTARY) || btnType.equalsIgnoreCase(SHELLY_BTNT_MOM_ON_RELEASE)
317 || btnType.equalsIgnoreCase(SHELLY_BTNT_ONE_BUTTON) || btnType.equalsIgnoreCase(SHELLY_BTNT_TWO_BUTTON);
320 public int getRollerFav(int id) {
321 if ((id >= 0) && getBool(settings.favoritesEnabled) && (settings.favorites != null)
322 && (id < settings.favorites.size())) {
323 return settings.favorites.get(id).pos;
328 public static String extractFwVersion(@Nullable String version) {
329 if (version != null) {
330 Matcher matcher = VERSION_PATTERN.matcher(version);
331 if (matcher.find()) {
332 // e.g. 20210226-091047/v1.10.0-rc2-89-g623b41ec0-master
333 return matcher.group(0);
339 public boolean coiotEnabled() {
340 if ((settings.coiot != null) && (settings.coiot.enabled != null)) {
341 return settings.coiot.enabled;
344 // If device is not yet intialized or the enabled property is missing we assume that CoIoT is enabled