2 * Copyright (c) 2010-2020 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.coap;
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.List;
22 import org.eclipse.jdt.annotation.NonNullByDefault;
23 import org.openhab.binding.shelly.internal.api.ShellyDeviceProfile;
24 import org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO.CoIotDescrBlk;
25 import org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO.CoIotDescrSen;
26 import org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO.CoIotSensor;
27 import org.openhab.binding.shelly.internal.handler.ShellyBaseHandler;
28 import org.openhab.binding.shelly.internal.handler.ShellyColorUtils;
29 import org.openhab.core.library.types.OnOffType;
30 import org.openhab.core.library.types.OpenClosedType;
31 import org.openhab.core.library.types.StringType;
32 import org.openhab.core.library.unit.Units;
33 import org.openhab.core.types.State;
34 import org.slf4j.Logger;
35 import org.slf4j.LoggerFactory;
38 * The {@link ShellyCoIoTProtocol} implements common functions for the CoIoT implementations
40 * @author Markus Michels - Initial contribution
43 public class ShellyCoIoTProtocol {
44 private final Logger logger = LoggerFactory.getLogger(ShellyCoIoTProtocol.class);
45 protected final String thingName;
46 protected final ShellyBaseHandler thingHandler;
47 protected final ShellyDeviceProfile profile;
48 protected final Map<String, CoIotDescrBlk> blkMap;
49 protected final Map<String, CoIotDescrSen> sensorMap;
51 // Due to the fact that the device reports only the current/last status, but no real events, we need to distinguish
52 // between a real update or just a repeated status on periodic updates
53 protected int lastCfgCount = -1;
54 protected int[] lastEventCount = { -1, -1, -1, -1, -1, -1, -1, -1 }; // 4Pro has 4 relays, so 8 should be fine
55 protected String[] inputEvent = { "", "", "", "", "", "", "", "" };
57 public ShellyCoIoTProtocol(String thingName, ShellyBaseHandler thingHandler, Map<String, CoIotDescrBlk> blkMap,
58 Map<String, CoIotDescrSen> sensorMap) {
59 this.thingName = thingName;
60 this.thingHandler = thingHandler;
62 this.sensorMap = sensorMap;
63 this.profile = thingHandler.getProfile();
66 protected boolean handleStatusUpdate(List<CoIotSensor> sensorUpdates, CoIotDescrSen sen, CoIotSensor s,
67 Map<String, State> updates) {
68 // Process status information and convert into channel updates
69 // Integer rIndex = Integer.parseInt(sen.links) + 1;
70 // String rGroup = getProfile().numRelays <= 1 ? CHANNEL_GROUP_RELAY_CONTROL
71 // : CHANNEL_GROUP_RELAY_CONTROL + rIndex;
72 int rIndex = getIdFromBlk(sen);
73 String rGroup = getProfile().numRelays <= 1 ? CHANNEL_GROUP_RELAY_CONTROL
74 : CHANNEL_GROUP_RELAY_CONTROL + rIndex;
75 switch (sen.type.toLowerCase()) {
76 case "b": // BatteryLevel +
77 updateChannel(updates, CHANNEL_GROUP_BATTERY, CHANNEL_SENSOR_BAT_LEVEL,
78 toQuantityType(s.value, DIGITS_PERCENT, Units.PERCENT));
80 case "h" /* Humidity */:
81 updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_HUM,
82 toQuantityType(s.value, DIGITS_PERCENT, Units.PERCENT));
84 case "m" /* Motion */:
85 updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_MOTION,
86 s.value == 1 ? OnOffType.ON : OnOffType.OFF);
88 case "l": // Luminosity +
89 updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_LUX,
90 toQuantityType(s.value, DIGITS_LUX, Units.LUX));
93 switch (sen.desc.toLowerCase()) {
94 case "state": // Relay status +
96 updatePower(profile, updates, rIndex, sen, s, sensorUpdates);
99 handleInput(sen, s, rGroup, updates);
102 // already handled by state/output
104 case "overtemp": // ++
106 thingHandler.postEvent(ALARM_TYPE_OVERTEMP, true);
110 // work around: Roller reports 101% instead max 100
111 double pos = Math.max(SHELLY_MIN_ROLLER_POS, Math.min(s.value, SHELLY_MAX_ROLLER_POS));
112 updateChannel(updates, CHANNEL_GROUP_ROL_CONTROL, CHANNEL_ROL_CONTROL_CONTROL,
113 toQuantityType(SHELLY_MAX_ROLLER_POS - pos, Units.PERCENT));
114 updateChannel(updates, CHANNEL_GROUP_ROL_CONTROL, CHANNEL_ROL_CONTROL_POS,
115 toQuantityType(pos, Units.PERCENT));
118 updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_FLOOD,
119 s.value == 1 ? OnOffType.ON : OnOffType.OFF);
121 case "vibration": // DW with FW1.6.5+
122 updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_VIBRATION,
123 s.value == 1 ? OnOffType.ON : OnOffType.OFF);
125 case "luminositylevel": // +
126 updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_ILLUM, getStringType(s.valueStr));
128 case "charger": // Sense
129 updateChannel(updates, CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_CHARGER,
130 s.value == 1 ? OnOffType.ON : OnOffType.OFF);
134 updateChannel(updates, CHANNEL_GROUP_COLOR_CONTROL, CHANNEL_COLOR_RED,
135 ShellyColorUtils.toPercent((int) s.value));
138 updateChannel(updates, CHANNEL_GROUP_COLOR_CONTROL, CHANNEL_COLOR_GREEN,
139 ShellyColorUtils.toPercent((int) s.value));
142 updateChannel(updates, CHANNEL_GROUP_COLOR_CONTROL, CHANNEL_COLOR_BLUE,
143 ShellyColorUtils.toPercent((int) s.value));
146 updateChannel(updates, CHANNEL_GROUP_COLOR_CONTROL, CHANNEL_COLOR_WHITE,
147 ShellyColorUtils.toPercent((int) s.value));
150 updateChannel(updates, CHANNEL_GROUP_COLOR_CONTROL, CHANNEL_COLOR_GAIN,
151 ShellyColorUtils.toPercent((int) s.value, SHELLY_MIN_GAIN, SHELLY_MAX_GAIN));
153 case "sensorerror": // +
154 updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_ERROR, getStringType(s.valueStr));
170 protected boolean updateChannel(Map<String, State> updates, String group, String channel, State value) {
171 updates.put(mkChannelId(group, channel), value);
175 protected void handleInput(CoIotDescrSen sen, CoIotSensor s, String rGroup, Map<String, State> updates) {
176 int idx = getSensorNumber(sen.desc, sen.id) - 1;
177 String iGroup = profile.getInputGroup(idx);
178 String iChannel = profile.getInputChannel(idx);
179 updateChannel(updates, iGroup, iChannel, s.value == 0 ? OnOffType.OFF : OnOffType.ON);
182 protected void handleInputEvent(CoIotDescrSen sen, String type, Integer count, Map<String, State> updates) {
183 int idx = getSensorNumber(sen.desc, sen.id) - 1;
184 String group = profile.getInputGroup(idx);
187 updateChannel(updates, group, CHANNEL_STATUS_EVENTTYPE, new StringType(type));
188 inputEvent[idx] = type;
191 updateChannel(updates, group, CHANNEL_STATUS_EVENTCOUNT, getDecimal(count));
192 if (profile.inButtonMode(idx) && ((profile.hasBattery && (count == 1)) || (count != lastEventCount[idx]))) {
193 if (profile.isButton || (lastEventCount[idx] != -1)) { // skip the first one if binding was restarted
194 thingHandler.triggerButton(group, inputEvent[idx]);
196 lastEventCount[idx] = count;
203 * Handles the combined updated of the brightness channel:
204 * brightness$Switch is the OnOffType (power state)
205 * brightness&Value is the brightness value
207 * @param profile Device profile, required to select the channel group and name
208 * @param updates List of updates. updatePower will add brightness$Switch and brightness&Value if changed
209 * @param id Sensor id from the update
210 * @param sen Sensor description from the update
211 * @param s New sensor value
212 * @param allUpdatesList of updates. This is required, because we need to update both values at the same time
214 protected void updatePower(ShellyDeviceProfile profile, Map<String, State> updates, int id, CoIotDescrSen sen,
215 CoIotSensor s, List<CoIotSensor> allUpdates) {
217 String channel = CHANNEL_BRIGHTNESS;
218 String checkL = ""; // RGBW-white uses 4 different Power, Brightness, VSwitch values
219 if (profile.isLight || profile.isDimmer) {
220 if (profile.isBulb || profile.inColor) {
221 group = CHANNEL_GROUP_LIGHT_CONTROL;
222 channel = CHANNEL_LIGHT_POWER;
223 } else if (profile.isDuo) {
224 group = CHANNEL_GROUP_WHITE_CONTROL;
225 } else if (profile.isDimmer) {
226 group = CHANNEL_GROUP_RELAY_CONTROL;
227 } else if (profile.isRGBW2) {
228 group = CHANNEL_GROUP_LIGHT_CHANNEL + id;
229 checkL = String.valueOf(id - 1); // id is 1-based, L is 0-based
230 logger.trace("{}: updatePower() for L={}", thingName, checkL);
233 // We need to update brigthtess and on/off state at the same time to avoid "flipping brightness slider" in
235 Double brightness = -1.0;
237 for (CoIotSensor update : allUpdates) {
238 CoIotDescrSen d = fixDescription(sensorMap.getOrDefault(update.id, new CoIotDescrSen()), blkMap);
239 if (!checkL.isEmpty() && !d.links.equals(checkL)) {
240 // continue until we find the correct one
243 if (d.desc.equalsIgnoreCase("brightness")) {
244 brightness = new Double(update.value);
245 } else if (d.desc.equalsIgnoreCase("output") || d.desc.equalsIgnoreCase("state")) {
246 power = new Double(update.value);
250 updateChannel(updates, group, channel + "$Switch", power == 1 ? OnOffType.ON : OnOffType.OFF);
252 if (brightness != -1) {
253 updateChannel(updates, group, channel + "$Value",
254 toQuantityType(power == 1 ? brightness : 0, DIGITS_NONE, Units.PERCENT));
256 } else if (profile.hasRelays) {
257 group = profile.numRelays <= 1 ? CHANNEL_GROUP_RELAY_CONTROL : CHANNEL_GROUP_RELAY_CONTROL + id;
258 updateChannel(updates, group, CHANNEL_OUTPUT, s.value == 1 ? OnOffType.ON : OnOffType.OFF);
259 } else if (profile.isSensor) {
261 if (profile.isDW) { // Door Window has item type Contact
262 updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_CONTACT,
263 s.value != 0 ? OpenClosedType.OPEN : OpenClosedType.CLOSED);
265 updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_CONTACT,
266 s.value == 1 ? OnOffType.ON : OnOffType.OFF);
272 * Find index of Input id, which is required to map to channel name
274 * @parm sensorDesc D field from sensor update
275 * @param sensorId The id from the sensor update
276 * @return Index of found entry (+1 will be the suffix for the channel name) or null if sensorId is not found
278 protected int getSensorNumber(String sensorDesc, String sensorId) {
280 for (Map.Entry<String, CoIotDescrSen> se : sensorMap.entrySet()) {
281 CoIotDescrSen sen = se.getValue();
282 if (sen.desc.equalsIgnoreCase(sensorDesc)) {
283 idx++; // iterate from input1..2..n
285 if (sen.id.equalsIgnoreCase(sensorId) && blkMap.containsKey(sen.links)) {
286 int id = getIdFromBlk(sen);
291 if (sen.id.equalsIgnoreCase(sensorId)) {
295 logger.debug("{}: sensorId {} not found in sensorMap!", thingName, sensorId);
299 protected int getIdFromBlk(CoIotDescrSen sen) {
301 if (blkMap.containsKey(sen.links)) {
302 CoIotDescrBlk blk = blkMap.get(sen.links);
303 String desc = blk.desc.toLowerCase();
304 if (desc.startsWith(SHELLY_CLASS_RELAY) || desc.startsWith(SHELLY_CLASS_ROLLER)
305 || desc.startsWith(SHELLY_CLASS_EMETER)) {
306 if (desc.contains("_")) { // CoAP v2
307 idx = Integer.parseInt(substringAfter(desc, "_"));
309 if (desc.substring(0, 5).equalsIgnoreCase(SHELLY_CLASS_RELAY)) {
310 idx = Integer.parseInt(substringAfter(desc, SHELLY_CLASS_RELAY));
312 if (desc.substring(0, 6).equalsIgnoreCase(SHELLY_CLASS_ROLLER)) {
313 idx = Integer.parseInt(substringAfter(desc, SHELLY_CLASS_ROLLER));
315 if (desc.substring(0, SHELLY_CLASS_EMETER.length()).equalsIgnoreCase(SHELLY_CLASS_EMETER)) {
316 idx = Integer.parseInt(substringAfter(desc, SHELLY_CLASS_EMETER));
319 idx = idx + 1; // make it 1-based (sen.L is 0-based)
327 * Get matching sensorId for updates on "External Temperature" - there might be more than 1 sensor.
329 * @param sensorId sensorId to map into a channel index
330 * @return Index of the corresponding channel (e.g. 0 build temperature1, 1->temperagture2...)
332 protected int getExtTempId(String sensorId) {
334 for (Map.Entry<String, CoIotDescrSen> se : sensorMap.entrySet()) {
335 CoIotDescrSen sen = se.getValue();
336 if (sen.desc.equalsIgnoreCase("external_temperature") || sen.desc.equalsIgnoreCase("external temperature c")
337 || (sen.desc.equalsIgnoreCase("extTemp") && !sen.unit.equalsIgnoreCase(SHELLY_TEMP_FAHRENHEIT))) {
338 idx++; // iterate from temperature1..2..n
340 if (sen.id.equalsIgnoreCase(sensorId)) {
344 logger.debug("{}: sensorId {} not found in sensorMap!", thingName, sensorId);
348 protected ShellyDeviceProfile getProfile() {
352 public CoIotDescrSen fixDescription(CoIotDescrSen sen, Map<String, CoIotDescrBlk> blkMap) {