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