]> git.basschouten.com Git - openhab-addons.git/blob
fcf633c5f10c452f05401f52f683bc09e725cf14
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2020 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.coap;
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.List;
20 import java.util.Map;
21
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.SmartHomeUnits;
33 import org.openhab.core.types.State;
34 import org.slf4j.Logger;
35 import org.slf4j.LoggerFactory;
36
37 /**
38  * The {@link ShellyCoIoTProtocol} implements common functions for the CoIoT implementations
39  *
40  * @author Markus Michels - Initial contribution
41  */
42 @NonNullByDefault
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;
50
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 = { "", "", "", "", "", "", "", "" };
56
57     public ShellyCoIoTProtocol(String thingName, ShellyBaseHandler thingHandler, Map<String, CoIotDescrBlk> blkMap,
58             Map<String, CoIotDescrSen> sensorMap) {
59         this.thingName = thingName;
60         this.thingHandler = thingHandler;
61         this.blkMap = blkMap;
62         this.sensorMap = sensorMap;
63         this.profile = thingHandler.getProfile();
64     }
65
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, SmartHomeUnits.PERCENT));
79                 break;
80             case "h" /* Humidity */:
81                 updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_HUM,
82                         toQuantityType(s.value, DIGITS_PERCENT, SmartHomeUnits.PERCENT));
83                 break;
84             case "m" /* Motion */:
85                 updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_MOTION,
86                         s.value == 1 ? OnOffType.ON : OnOffType.OFF);
87                 break;
88             case "l": // Luminosity +
89                 updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_LUX,
90                         toQuantityType(s.value, DIGITS_LUX, SmartHomeUnits.LUX));
91                 break;
92             case "s": // CatchAll
93                 switch (sen.desc.toLowerCase()) {
94                     case "state": // Relay status +
95                     case "output":
96                         updatePower(profile, updates, rIndex, sen, s, sensorUpdates);
97                         break;
98                     case "input":
99                         handleInput(sen, s, rGroup, updates);
100                         break;
101                     case "brightness":
102                         // already handled by state/output
103                         break;
104                     case "overtemp": // ++
105                         if (s.value == 1) {
106                             thingHandler.postEvent(ALARM_TYPE_OVERTEMP, true);
107                         }
108                         break;
109                     case "position":
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, SmartHomeUnits.PERCENT));
114                         updateChannel(updates, CHANNEL_GROUP_ROL_CONTROL, CHANNEL_ROL_CONTROL_POS,
115                                 toQuantityType(pos, SmartHomeUnits.PERCENT));
116                         break;
117                     case "flood":
118                         updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_FLOOD,
119                                 s.value == 1 ? OnOffType.ON : OnOffType.OFF);
120                         break;
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);
124                         break;
125                     case "luminositylevel": // +
126                         updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_ILLUM, getStringType(s.valueStr));
127                         break;
128                     case "charger": // Sense
129                         updateChannel(updates, CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_CHARGER,
130                                 s.value == 1 ? OnOffType.ON : OnOffType.OFF);
131                         break;
132                     // RGBW2/Bulb
133                     case "red":
134                         updateChannel(updates, CHANNEL_GROUP_COLOR_CONTROL, CHANNEL_COLOR_RED,
135                                 ShellyColorUtils.toPercent((int) s.value));
136                         break;
137                     case "green":
138                         updateChannel(updates, CHANNEL_GROUP_COLOR_CONTROL, CHANNEL_COLOR_GREEN,
139                                 ShellyColorUtils.toPercent((int) s.value));
140                         break;
141                     case "blue":
142                         updateChannel(updates, CHANNEL_GROUP_COLOR_CONTROL, CHANNEL_COLOR_BLUE,
143                                 ShellyColorUtils.toPercent((int) s.value));
144                         break;
145                     case "white":
146                         updateChannel(updates, CHANNEL_GROUP_COLOR_CONTROL, CHANNEL_COLOR_WHITE,
147                                 ShellyColorUtils.toPercent((int) s.value));
148                         break;
149                     case "gain":
150                         updateChannel(updates, CHANNEL_GROUP_COLOR_CONTROL, CHANNEL_COLOR_GAIN,
151                                 ShellyColorUtils.toPercent((int) s.value, SHELLY_MIN_GAIN, SHELLY_MAX_GAIN));
152                         break;
153                     case "sensorerror": // +
154                         updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_ERROR, getStringType(s.valueStr));
155                         break;
156                     default:
157                         // Unknown
158                         return false;
159                 }
160                 break;
161
162             default:
163                 // Unknown type
164                 return false;
165         }
166
167         return true;
168     }
169
170     protected boolean updateChannel(Map<String, State> updates, String group, String channel, State value) {
171         updates.put(mkChannelId(group, channel), value);
172         return true;
173     }
174
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);
180     }
181
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);
185         if (count == -1) {
186             // event type
187             updateChannel(updates, group, CHANNEL_STATUS_EVENTTYPE, new StringType(type));
188             inputEvent[idx] = type;
189         } else {
190             // event count
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]);
195                 }
196                 lastEventCount[idx] = count;
197             }
198         }
199     }
200
201     /**
202      *
203      * Handles the combined updated of the brightness channel:
204      * brightness$Switch is the OnOffType (power state)
205      * brightness&Value is the brightness value
206      *
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
213      */
214     protected void updatePower(ShellyDeviceProfile profile, Map<String, State> updates, int id, CoIotDescrSen sen,
215             CoIotSensor s, List<CoIotSensor> allUpdates) {
216         String group = "";
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);
231             }
232
233             // We need to update brigthtess and on/off state at the same time to avoid "flipping brightness slider" in
234             // the UI
235             Double brightness = -1.0;
236             Double power = -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
241                     continue;
242                 }
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);
247                 }
248             }
249             if (power != -1) {
250                 updateChannel(updates, group, channel + "$Switch", power == 1 ? OnOffType.ON : OnOffType.OFF);
251             }
252             if (brightness != -1) {
253                 updateChannel(updates, group, channel + "$Value",
254                         toQuantityType(power == 1 ? brightness : 0, DIGITS_NONE, SmartHomeUnits.PERCENT));
255             }
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) {
260             // Sensor state
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);
264             } else {
265                 updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_CONTACT,
266                         s.value == 1 ? OnOffType.ON : OnOffType.OFF);
267             }
268         }
269     }
270
271     /**
272      * Find index of Input id, which is required to map to channel name
273      *
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
277      */
278     protected int getSensorNumber(String sensorDesc, String sensorId) {
279         int idx = 0;
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
284             }
285             if (sen.id.equalsIgnoreCase(sensorId) && blkMap.containsKey(sen.links)) {
286                 int id = getIdFromBlk(sen);
287                 if (id != -1) {
288                     return id;
289                 }
290             }
291             if (sen.id.equalsIgnoreCase(sensorId)) {
292                 return idx;
293             }
294         }
295         logger.debug("{}: sensorId {} not found in sensorMap!", thingName, sensorId);
296         return -1;
297     }
298
299     protected int getIdFromBlk(CoIotDescrSen sen) {
300         int idx = -1;
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, "_"));
308                 } else { // CoAP v1
309                     if (desc.substring(0, 5).equalsIgnoreCase(SHELLY_CLASS_RELAY)) {
310                         idx = Integer.parseInt(substringAfter(desc, SHELLY_CLASS_RELAY));
311                     }
312                     if (desc.substring(0, 6).equalsIgnoreCase(SHELLY_CLASS_ROLLER)) {
313                         idx = Integer.parseInt(substringAfter(desc, SHELLY_CLASS_ROLLER));
314                     }
315                     if (desc.substring(0, SHELLY_CLASS_EMETER.length()).equalsIgnoreCase(SHELLY_CLASS_EMETER)) {
316                         idx = Integer.parseInt(substringAfter(desc, SHELLY_CLASS_EMETER));
317                     }
318                 }
319                 idx = idx + 1; // make it 1-based (sen.L is 0-based)
320             }
321         }
322         return idx;
323     }
324
325     /**
326      *
327      * Get matching sensorId for updates on "External Temperature" - there might be more than 1 sensor.
328      *
329      * @param sensorId sensorId to map into a channel index
330      * @return Index of the corresponding channel (e.g. 0 build temperature1, 1->temperagture2...)
331      */
332     protected int getExtTempId(String sensorId) {
333         int idx = 0;
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
339             }
340             if (sen.id.equalsIgnoreCase(sensorId)) {
341                 return idx;
342             }
343         }
344         logger.debug("{}: sensorId {} not found in sensorMap!", thingName, sensorId);
345         return -1;
346     }
347
348     protected ShellyDeviceProfile getProfile() {
349         return profile;
350     }
351
352     public CoIotDescrSen fixDescription(CoIotDescrSen sen, Map<String, CoIotDescrBlk> blkMap) {
353         return sen;
354     }
355 }