]> git.basschouten.com Git - openhab-addons.git/blob
db9d7f614af1efeafcf4458c6175ad1f5f875d24
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2021 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.eclipse.jdt.annotation.Nullable;
24 import org.openhab.binding.shelly.internal.api.ShellyDeviceProfile;
25 import org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO.CoIotDescrBlk;
26 import org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO.CoIotDescrSen;
27 import org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO.CoIotSensor;
28 import org.openhab.binding.shelly.internal.handler.ShellyBaseHandler;
29 import org.openhab.binding.shelly.internal.handler.ShellyColorUtils;
30 import org.openhab.core.library.types.OnOffType;
31 import org.openhab.core.library.types.OpenClosedType;
32 import org.openhab.core.library.types.StringType;
33 import org.openhab.core.library.unit.Units;
34 import org.openhab.core.types.State;
35 import org.slf4j.Logger;
36 import org.slf4j.LoggerFactory;
37
38 import com.google.gson.Gson;
39 import com.google.gson.GsonBuilder;
40 import com.google.gson.JsonSyntaxException;
41
42 /**
43  * The {@link ShellyCoIoTProtocol} implements common functions for the CoIoT implementations
44  *
45  * @author Markus Michels - Initial contribution
46  */
47 @NonNullByDefault
48 public class ShellyCoIoTProtocol {
49     private final Logger logger = LoggerFactory.getLogger(ShellyCoIoTProtocol.class);
50     protected final String thingName;
51     protected final ShellyBaseHandler thingHandler;
52     protected final ShellyDeviceProfile profile;
53     protected final Map<String, CoIotDescrBlk> blkMap;
54     protected final Map<String, CoIotDescrSen> sensorMap;
55     private final Gson gson = new GsonBuilder().create();
56
57     // Due to the fact that the device reports only the current/last status, but no real events, we need to distinguish
58     // between a real update or just a repeated status on periodic updates
59     protected int lastCfgCount = -1;
60     protected int[] lastEventCount = { -1, -1, -1, -1, -1, -1, -1, -1 }; // 4Pro has 4 relays, so 8 should be fine
61     protected String[] inputEvent = { "", "", "", "", "", "", "", "" };
62     protected String lastWakeup = "";
63
64     public ShellyCoIoTProtocol(String thingName, ShellyBaseHandler thingHandler, Map<String, CoIotDescrBlk> blkMap,
65             Map<String, CoIotDescrSen> sensorMap) {
66         this.thingName = thingName;
67         this.thingHandler = thingHandler;
68         this.blkMap = blkMap;
69         this.sensorMap = sensorMap;
70         this.profile = thingHandler.getProfile();
71     }
72
73     protected boolean handleStatusUpdate(List<CoIotSensor> sensorUpdates, CoIotDescrSen sen, CoIotSensor s,
74             Map<String, State> updates, ShellyColorUtils col) {
75         // Process status information and convert into channel updates
76         // Integer rIndex = Integer.parseInt(sen.links) + 1;
77         // String rGroup = getProfile().numRelays <= 1 ? CHANNEL_GROUP_RELAY_CONTROL
78         // : CHANNEL_GROUP_RELAY_CONTROL + rIndex;
79         int rIndex = getIdFromBlk(sen);
80         String rGroup = getProfile().numRelays <= 1 ? CHANNEL_GROUP_RELAY_CONTROL
81                 : CHANNEL_GROUP_RELAY_CONTROL + rIndex;
82
83         switch (sen.type.toLowerCase()) {
84             case "b": // BatteryLevel +
85                 updateChannel(updates, CHANNEL_GROUP_BATTERY, CHANNEL_SENSOR_BAT_LEVEL,
86                         toQuantityType(s.value, DIGITS_PERCENT, Units.PERCENT));
87                 break;
88             case "h" /* Humidity */:
89                 updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_HUM,
90                         toQuantityType(s.value, DIGITS_PERCENT, Units.PERCENT));
91                 break;
92             case "m" /* Motion */:
93                 updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_MOTION,
94                         s.value == 1 ? OnOffType.ON : OnOffType.OFF);
95                 break;
96             case "l": // Luminosity +
97                 updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_LUX,
98                         toQuantityType(s.value, DIGITS_LUX, Units.LUX));
99                 break;
100             case "s": // CatchAll
101                 switch (sen.desc.toLowerCase()) {
102                     case "state": // Relay status +
103                     case "output":
104                         updatePower(profile, updates, rIndex, sen, s, sensorUpdates);
105                         break;
106                     case "input":
107                         handleInput(sen, s, rGroup, updates);
108                         break;
109                     case "brightness":
110                         // already handled by state/output
111                         break;
112                     case "overtemp": // ++
113                         if (s.value == 1) {
114                             thingHandler.postEvent(ALARM_TYPE_OVERTEMP, true);
115                         }
116                         break;
117                     case "position":
118                         // work around: Roller reports 101% instead max 100
119                         double pos = Math.max(SHELLY_MIN_ROLLER_POS, Math.min(s.value, SHELLY_MAX_ROLLER_POS));
120                         updateChannel(updates, CHANNEL_GROUP_ROL_CONTROL, CHANNEL_ROL_CONTROL_CONTROL,
121                                 toQuantityType(SHELLY_MAX_ROLLER_POS - pos, Units.PERCENT));
122                         updateChannel(updates, CHANNEL_GROUP_ROL_CONTROL, CHANNEL_ROL_CONTROL_POS,
123                                 toQuantityType(pos, Units.PERCENT));
124                         break;
125                     case "flood":
126                         updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_FLOOD,
127                                 s.value == 1 ? OnOffType.ON : OnOffType.OFF);
128                         break;
129                     case "vibration": // DW with FW1.6.5+
130                         updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_VIBRATION,
131                                 s.value == 1 ? OnOffType.ON : OnOffType.OFF);
132                         break;
133                     case "luminositylevel": // +
134                         updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_ILLUM, getStringType(s.valueStr));
135                         break;
136                     case "charger": // Sense
137                         updateChannel(updates, CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_CHARGER,
138                                 s.value == 1 ? OnOffType.ON : OnOffType.OFF);
139                         break;
140                     // RGBW2/Bulb
141                     case "red":
142                         col.setRed((int) s.value);
143                         updateChannel(updates, CHANNEL_GROUP_COLOR_CONTROL, CHANNEL_COLOR_RED,
144                                 ShellyColorUtils.toPercent((int) s.value));
145                         break;
146                     case "green":
147                         col.setGreen((int) s.value);
148                         updateChannel(updates, CHANNEL_GROUP_COLOR_CONTROL, CHANNEL_COLOR_GREEN,
149                                 ShellyColorUtils.toPercent((int) s.value));
150                         break;
151                     case "blue":
152                         col.setBlue((int) s.value);
153                         updateChannel(updates, CHANNEL_GROUP_COLOR_CONTROL, CHANNEL_COLOR_BLUE,
154                                 ShellyColorUtils.toPercent((int) s.value));
155                         break;
156                     case "white":
157                         col.setWhite((int) s.value);
158                         updateChannel(updates, CHANNEL_GROUP_COLOR_CONTROL, CHANNEL_COLOR_WHITE,
159                                 ShellyColorUtils.toPercent((int) s.value));
160                         break;
161                     case "gain":
162                         col.setGain((int) s.value);
163                         updateChannel(updates, CHANNEL_GROUP_COLOR_CONTROL, CHANNEL_COLOR_GAIN,
164                                 ShellyColorUtils.toPercent((int) s.value, SHELLY_MIN_GAIN, SHELLY_MAX_GAIN));
165                         break;
166                     case "sensorerror": // +
167                         updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_ERROR, getStringType(s.valueStr));
168                         break;
169                     default:
170                         // Unknown
171                         return false;
172                 }
173                 break;
174
175             default:
176                 // Unknown type
177                 return false;
178         }
179
180         return true;
181     }
182
183     public static boolean updateChannel(Map<String, State> updates, String group, String channel, State value) {
184         updates.put(mkChannelId(group, channel), value);
185         return true;
186     }
187
188     protected void handleInput(CoIotDescrSen sen, CoIotSensor s, String rGroup, Map<String, State> updates) {
189         int idx = getSensorNumber(sen.desc, sen.id) - 1;
190         String iGroup = profile.getInputGroup(idx);
191         String iChannel = CHANNEL_INPUT + profile.getInputSuffix(idx);
192         updateChannel(updates, iGroup, iChannel, s.value == 0 ? OnOffType.OFF : OnOffType.ON);
193     }
194
195     protected void handleInputEvent(CoIotDescrSen sen, String type, int count, int serial, Map<String, State> updates) {
196         int idx = getSensorNumber(sen.desc, sen.id) - 1;
197         String group = profile.getInputGroup(idx);
198         if (count == -1) {
199             // event type
200             updateChannel(updates, group, CHANNEL_STATUS_EVENTTYPE + profile.getInputSuffix(idx), new StringType(type));
201             inputEvent[idx] = type;
202         } else {
203             // event count
204             updateChannel(updates, group, CHANNEL_STATUS_EVENTCOUNT + profile.getInputSuffix(idx), getDecimal(count));
205             logger.trace(
206                     "{}: Check button[{}] for event trigger (isButtonMode={}, isButton={}, hasBattery={}, serial={}, count={}, lastEventCount[{}]={}",
207                     thingName, idx, profile.inButtonMode(idx), profile.isButton, profile.hasBattery, serial, count, idx,
208                     lastEventCount[idx]);
209             if (profile.inButtonMode(idx) && ((profile.hasBattery && (count == 1))
210                     || ((lastEventCount[idx] != -1) && (count != lastEventCount[idx])))) {
211                 if (!profile.isButton || (profile.isButton && (serial != 0x200))) { // skip duplicate on wake-up
212                     logger.debug("{}: Trigger event {}", thingName, inputEvent[idx]);
213                     thingHandler.triggerButton(group, idx, inputEvent[idx]);
214                 }
215             }
216             lastEventCount[idx] = count;
217         }
218     }
219
220     /**
221      *
222      * Handles the combined updated of the brightness channel:
223      * brightness$Switch is the OnOffType (power state)
224      * brightness&Value is the brightness value
225      *
226      * @param profile Device profile, required to select the channel group and name
227      * @param updates List of updates. updatePower will add brightness$Switch and brightness&Value if changed
228      * @param id Sensor id from the update
229      * @param sen Sensor description from the update
230      * @param s New sensor value
231      * @param allUpdatesList of updates. This is required, because we need to update both values at the same time
232      */
233     protected void updatePower(ShellyDeviceProfile profile, Map<String, State> updates, int id, CoIotDescrSen sen,
234             CoIotSensor s, List<CoIotSensor> allUpdates) {
235         String group = "";
236         String channel = CHANNEL_BRIGHTNESS;
237         String checkL = ""; // RGBW-white uses 4 different Power, Brightness, VSwitch values
238         if (profile.isLight || profile.isDimmer) {
239             if (profile.isBulb || profile.inColor) {
240                 group = CHANNEL_GROUP_LIGHT_CONTROL;
241                 channel = CHANNEL_LIGHT_POWER;
242             } else if (profile.isDuo) {
243                 group = CHANNEL_GROUP_WHITE_CONTROL;
244             } else if (profile.isDimmer) {
245                 group = CHANNEL_GROUP_RELAY_CONTROL;
246             } else if (profile.isRGBW2) {
247                 checkL = String.valueOf(id); // String.valueOf(id - 1); // id is 1-based, L is 0-based
248                 group = CHANNEL_GROUP_LIGHT_CHANNEL + id;
249                 logger.trace("{}: updatePower() for L={}", thingName, checkL);
250             }
251
252             // We need to update brightness and on/off state at the same time to avoid "flipping brightness slider" in
253             // the UI
254             double brightness = -1.0;
255             double power = -1.0;
256             for (CoIotSensor update : allUpdates) {
257                 CoIotDescrSen d = fixDescription(sensorMap.get(update.id), blkMap);
258                 if (!checkL.isEmpty() && !d.links.equals(checkL)) {
259                     // continue until we find the correct one
260                     continue;
261                 }
262                 if (d.desc.equalsIgnoreCase("brightness")) {
263                     brightness = update.value;
264                 } else if (d.desc.equalsIgnoreCase("output") || d.desc.equalsIgnoreCase("state")) {
265                     power = update.value;
266                 }
267             }
268             if (power != -1) {
269                 updateChannel(updates, group, channel + "$Switch", power == 1 ? OnOffType.ON : OnOffType.OFF);
270             }
271             if (brightness != -1) {
272                 updateChannel(updates, group, channel + "$Value",
273                         toQuantityType(power == 1 ? brightness : 0, DIGITS_NONE, Units.PERCENT));
274             }
275         } else if (profile.hasRelays) {
276             group = profile.numRelays <= 1 ? CHANNEL_GROUP_RELAY_CONTROL : CHANNEL_GROUP_RELAY_CONTROL + id;
277             updateChannel(updates, group, CHANNEL_OUTPUT, s.value == 1 ? OnOffType.ON : OnOffType.OFF);
278         } else if (profile.isSensor) {
279             // Sensor state
280             if (profile.isDW) { // Door Window has item type Contact
281                 updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_CONTACT,
282                         s.value != 0 ? OpenClosedType.OPEN : OpenClosedType.CLOSED);
283             } else {
284                 updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_CONTACT,
285                         s.value == 1 ? OnOffType.ON : OnOffType.OFF);
286             }
287         }
288     }
289
290     /**
291      * Find index of Input id, which is required to map to channel name
292      *
293      * @parm sensorDesc D field from sensor update
294      * @param sensorId The id from the sensor update
295      * @return Index of found entry (+1 will be the suffix for the channel name) or null if sensorId is not found
296      */
297     protected int getSensorNumber(String sensorDesc, String sensorId) {
298         int idx = 0;
299         for (Map.Entry<String, CoIotDescrSen> se : sensorMap.entrySet()) {
300             CoIotDescrSen sen = se.getValue();
301             if (sen.desc.equalsIgnoreCase(sensorDesc)) {
302                 idx++; // iterate from input1..2..n
303             }
304             if (sen.id.equalsIgnoreCase(sensorId) && blkMap.containsKey(sen.links)) {
305                 int id = getIdFromBlk(sen);
306                 if (id != -1) {
307                     return id;
308                 }
309             }
310             if (sen.id.equalsIgnoreCase(sensorId)) {
311                 return idx;
312             }
313         }
314         logger.debug("{}: sensorId {} not found in sensorMap!", thingName, sensorId);
315         return -1;
316     }
317
318     protected int getIdFromBlk(CoIotDescrSen sen) {
319         int idx = -1;
320         CoIotDescrBlk blk = blkMap.get(sen.links);
321         if (blk != null) {
322             String desc = blk.desc.toLowerCase();
323             if (desc.startsWith(SHELLY_CLASS_RELAY) || desc.startsWith(SHELLY_CLASS_ROLLER)
324                     || desc.startsWith(SHELLY_CLASS_LIGHT) || desc.startsWith(SHELLY_CLASS_EMETER)) {
325                 if (desc.contains("_")) { // CoAP v2
326                     idx = Integer.parseInt(substringAfter(desc, "_"));
327                 } else { // CoAP v1
328                     if (desc.substring(0, 5).equalsIgnoreCase(SHELLY_CLASS_RELAY)) {
329                         idx = Integer.parseInt(substringAfter(desc, SHELLY_CLASS_RELAY));
330                     }
331                     if (desc.substring(0, 6).equalsIgnoreCase(SHELLY_CLASS_ROLLER)) {
332                         idx = Integer.parseInt(substringAfter(desc, SHELLY_CLASS_ROLLER));
333                     }
334                     if (desc.substring(0, SHELLY_CLASS_EMETER.length()).equalsIgnoreCase(SHELLY_CLASS_EMETER)) {
335                         idx = Integer.parseInt(substringAfter(desc, SHELLY_CLASS_EMETER));
336                     }
337                 }
338                 idx = idx + 1; // make it 1-based (sen.L is 0-based)
339             }
340         }
341         return idx;
342     }
343
344     /**
345      *
346      * Get matching sensorId for updates on "External Temperature" - there might be more than 1 sensor.
347      *
348      * @param sensorId sensorId to map into a channel index
349      * @return Index of the corresponding channel (e.g. 0 build temperature1, 1->temperagture2...)
350      */
351     protected int getExtTempId(String sensorId) {
352         int idx = 0;
353         for (Map.Entry<String, CoIotDescrSen> se : sensorMap.entrySet()) {
354             CoIotDescrSen sen = se.getValue();
355             if (sen.desc.equalsIgnoreCase("external_temperature") || sen.desc.equalsIgnoreCase("external temperature c")
356                     || (sen.desc.equalsIgnoreCase("extTemp") && !sen.unit.equalsIgnoreCase(SHELLY_TEMP_FAHRENHEIT))) {
357                 idx++; // iterate from temperature1..2..n
358             }
359             if (sen.id.equalsIgnoreCase(sensorId)) {
360                 return idx;
361             }
362         }
363         logger.debug("{}: sensorId {} not found in sensorMap!", thingName, sensorId);
364         return -1;
365     }
366
367     protected ShellyDeviceProfile getProfile() {
368         return profile;
369     }
370
371     public CoIotDescrSen fixDescription(@Nullable CoIotDescrSen sen, Map<String, CoIotDescrBlk> blkMap) {
372         return sen != null ? sen : new CoIotDescrSen();
373     }
374
375     public void completeMissingSensorDefinition(Map<String, CoIotDescrSen> sensorMap) {
376     }
377
378     protected void addSensor(Map<String, CoIotDescrSen> sensorMap, String key, String json) {
379         try {
380             if (!sensorMap.containsKey(key)) {
381                 CoIotDescrSen sen = gson.fromJson(json, CoIotDescrSen.class);
382                 if (sen != null) {
383                     sensorMap.put(key, sen);
384                 }
385             }
386         } catch (JsonSyntaxException e) {
387             // should never happen
388             logger.trace("Unable to parse sensor definition: {}", json, e);
389         }
390     }
391
392     public String getLastWakeup() {
393         return lastWakeup;
394     }
395 }