]> git.basschouten.com Git - openhab-addons.git/blob
101f967601d09a9b396097bc61659116b2a048c7
[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.coap.ShellyCoapJSonDTO.CoIotDescrBlk;
24 import org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO.CoIotDescrSen;
25 import org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO.CoIotSensor;
26 import org.openhab.binding.shelly.internal.handler.ShellyBaseHandler;
27 import org.openhab.binding.shelly.internal.handler.ShellyColorUtils;
28 import org.openhab.core.library.types.OnOffType;
29 import org.openhab.core.library.unit.ImperialUnits;
30 import org.openhab.core.library.unit.SIUnits;
31 import org.openhab.core.library.unit.SmartHomeUnits;
32 import org.openhab.core.types.State;
33 import org.slf4j.Logger;
34 import org.slf4j.LoggerFactory;
35
36 import tec.uom.se.unit.Units;
37
38 /**
39  * The {@link ShellyCoIoTVersion1} implements the parsing for CoIoT version 1
40  *
41  * @author Markus Michels - Initial contribution
42  */
43 @NonNullByDefault
44 public class ShellyCoIoTVersion1 extends ShellyCoIoTProtocol implements ShellyCoIoTInterface {
45     private final Logger logger = LoggerFactory.getLogger(ShellyCoIoTVersion1.class);
46
47     public ShellyCoIoTVersion1(String thingName, ShellyBaseHandler thingHandler, Map<String, CoIotDescrBlk> blkMap,
48             Map<String, CoIotDescrSen> sensorMap) {
49         super(thingName, thingHandler, blkMap, sensorMap);
50     }
51
52     @Override
53     public int getVersion() {
54         return ShellyCoapJSonDTO.COIOT_VERSION_1;
55     }
56
57     /**
58      * Process CoIoT status update message. If a status update is received, but the device description has not been
59      * received yet a GET is send to query device description.
60      *
61      * @param devId device id included in the status packet
62      * @param payload CoAP payload (Json format), example: {"G":[[0,112,0]]}
63      * @param serial Serial for this request. If this the the same as last serial
64      *            the update was already sent and processed so this one gets
65      *            ignored.
66      */
67     @Override
68     public boolean handleStatusUpdate(List<CoIotSensor> sensorUpdates, CoIotDescrSen sen, CoIotSensor s,
69             Map<String, State> updates) {
70         // first check the base implementation
71         if (super.handleStatusUpdate(sensorUpdates, sen, s, updates)) {
72             // process by the base class
73             return true;
74         }
75
76         // Process status information and convert into channel updates
77         Integer rIndex = Integer.parseInt(sen.links) + 1;
78         String rGroup = getProfile().numRelays <= 1 ? CHANNEL_GROUP_RELAY_CONTROL
79                 : CHANNEL_GROUP_RELAY_CONTROL + rIndex;
80         switch (sen.type.toLowerCase()) {
81             case "t": // Temperature +
82                 Double value = getDouble(s.value);
83                 switch (sen.desc.toLowerCase()) {
84                     case "temperature": // Sensor Temp
85                         if (getString(getProfile().settings.temperatureUnits)
86                                 .equalsIgnoreCase(SHELLY_TEMP_FAHRENHEIT)) {
87                             value = ImperialUnits.FAHRENHEIT.getConverterTo(Units.CELSIUS).convert(getDouble(s.value))
88                                     .doubleValue();
89                         }
90                         updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_TEMP,
91                                 toQuantityType(value, DIGITS_TEMP, SIUnits.CELSIUS));
92                         break;
93                     case "temperature f": // Device Temp -> ignore (we use C only)
94                         break;
95                     case "temperature c": // Device Temp in C
96                         // Device temperature
97                         updateChannel(updates, CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_ITEMP,
98                                 toQuantityType(value, DIGITS_NONE, SIUnits.CELSIUS));
99                         break;
100                     case "external temperature f": // Shelly 1/1PM external temp sensors
101                         // ignore F, we use C only
102                         break;
103                     case "external temperature c": // Shelly 1/1PM external temp sensors
104                     case "external_temperature":
105                         int idx = getExtTempId(sen.id);
106                         if (idx > 0) {
107                             updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_TEMP + idx,
108                                     toQuantityType(value, DIGITS_TEMP, SIUnits.CELSIUS));
109                         } else {
110                             logger.debug("{}: Unable to get extSensorId {} from {}/{}", thingName, sen.id, sen.type,
111                                     sen.desc);
112                         }
113                         break;
114                     default:
115                         logger.debug("{}: Unknown temperatur type: {}", thingName, sen.desc);
116                 }
117                 break;
118             case "p": // Power/Watt
119                 // 3EM uses 1-based meter IDs, other 0-based
120                 String mGroup = profile.numMeters == 1 ? CHANNEL_GROUP_METER
121                         : CHANNEL_GROUP_METER + (profile.isEMeter ? sen.links : rIndex);
122                 updateChannel(updates, mGroup, CHANNEL_METER_CURRENTWATTS,
123                         toQuantityType(s.value, DIGITS_WATT, SmartHomeUnits.WATT));
124                 updateChannel(updates, mGroup, CHANNEL_LAST_UPDATE, getTimestamp());
125                 break;
126             case "s" /* CatchAll */:
127                 switch (sen.desc.toLowerCase()) {
128                     case "overtemp":
129                         if (s.value == 1) {
130                             thingHandler.postEvent(ALARM_TYPE_OVERTEMP, true);
131                         }
132                         break;
133                     case "energy counter 0 [w-min]":
134                         updateChannel(updates, rGroup, CHANNEL_METER_LASTMIN1,
135                                 toQuantityType(s.value, DIGITS_WATT, SmartHomeUnits.WATT));
136                         break;
137                     case "energy counter 1 [w-min]":
138                     case "energy counter 2 [w-min]":
139                         // we don't use them
140                         break;
141                     case "energy counter total [w-h]": // 3EM reports W/h
142                     case "energy counter total [w-min]":
143                         Double total = profile.isEMeter ? s.value / 1000 : s.value / 60 / 1000;
144                         updateChannel(updates, rGroup, CHANNEL_METER_TOTALKWH,
145                                 toQuantityType(total, DIGITS_KWH, SmartHomeUnits.KILOWATT_HOUR));
146                         break;
147                     case "voltage":
148                         updateChannel(updates, rGroup, CHANNEL_EMETER_VOLTAGE,
149                                 toQuantityType(getDouble(s.value), DIGITS_VOLT, SmartHomeUnits.VOLT));
150                         break;
151                     case "current":
152                         updateChannel(updates, rGroup, CHANNEL_EMETER_CURRENT,
153                                 toQuantityType(getDouble(s.value), DIGITS_VOLT, SmartHomeUnits.AMPERE));
154                         break;
155                     case "pf":
156                         updateChannel(updates, rGroup, CHANNEL_EMETER_PFACTOR, getDecimal(s.value));
157                         break;
158                     case "position":
159                         // work around: Roller reports 101% instead max 100
160                         double pos = Math.max(SHELLY_MIN_ROLLER_POS, Math.min(s.value, SHELLY_MAX_ROLLER_POS));
161                         updateChannel(updates, CHANNEL_GROUP_ROL_CONTROL, CHANNEL_ROL_CONTROL_CONTROL,
162                                 toQuantityType(SHELLY_MAX_ROLLER_POS - pos, SmartHomeUnits.PERCENT));
163                         updateChannel(updates, CHANNEL_GROUP_ROL_CONTROL, CHANNEL_ROL_CONTROL_POS,
164                                 toQuantityType(pos, SmartHomeUnits.PERCENT));
165                         break;
166                     case "input event": // Shelly Button 1
167                         handleInputEvent(sen, getString(s.valueStr), -1, updates);
168                         break;
169                     case "input event counter": // Shelly Button 1/ix3
170                         handleInputEvent(sen, "", getInteger((int) s.value), updates);
171                         break;
172                     case "flood":
173                         updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_FLOOD,
174                                 s.value == 1 ? OnOffType.ON : OnOffType.OFF);
175                         break;
176                     case "tilt": // DW with FW1.6.5+ //+
177                         updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_TILT,
178                                 toQuantityType(s.value, DIGITS_NONE, SmartHomeUnits.DEGREE_ANGLE));
179                         break;
180                     case "vibration": // DW with FW1.6.5+
181                         updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_VIBRATION,
182                                 s.value == 1 ? OnOffType.ON : OnOffType.OFF);
183                         break;
184                     case "temp": // Shelly Bulb
185                     case "colortemperature": // Shelly Duo
186                         updateChannel(updates,
187                                 profile.inColor ? CHANNEL_GROUP_COLOR_CONTROL : CHANNEL_GROUP_WHITE_CONTROL,
188                                 CHANNEL_COLOR_TEMP,
189                                 ShellyColorUtils.toPercent((int) s.value, profile.minTemp, profile.maxTemp));
190                         break;
191                     case "sensor state": // Shelly Gas
192                         updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_SSTATE, getStringType(s.valueStr));
193                         break;
194                     case "alarm state": // Shelly Gas
195                         updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_ALARM_STATE,
196                                 getStringType(s.valueStr));
197                         break;
198                     case "self-test state":// Shelly Gas
199                         updateChannel(updates, CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_SELFTTEST,
200                                 getStringType(s.valueStr));
201                         break;
202                     case "concentration":// Shelly Gas
203                         updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_PPM, getDecimal(s.value));
204                         break;
205                     case "sensorerror":
206                         updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_ERROR, getStringType(s.valueStr));
207                         break;
208                     default:
209                         // Unknown
210                         return false;
211                 }
212                 break;
213
214             default:
215                 // Unknown type
216                 return false;
217         }
218         return true;
219     }
220
221     /**
222      *
223      * Depending on the device type and firmware release there are significant bugs or incosistencies in the CoIoT
224      * Device Description returned by the discovery request. Shelly is even not following it's own speicifcation. All of
225      * that has been reported to Shelly and acknowledged. Firmware 1.6 brought significant improvements. However, the
226      * old mapping stays in to support older firmware releases.
227      *
228      * @param sen Sensor description received from device
229      * @return fixed Sensor description (sen)
230      */
231     @Override
232     public CoIotDescrSen fixDescription(CoIotDescrSen sen, Map<String, CoIotDescrBlk> blkMap) {
233         // Shelly1: reports null descr+type "Switch" -> map to S
234         // Shelly1PM: reports null descr+type "Overtemp" -> map to O
235         // Shelly1PM: reports null descr+type "W" -> add description
236         // Shelly1PM: reports temp senmsors without desc -> add description
237         // Shelly Dimmer: sensors are reported without descriptions -> map to S
238         // SHelly Sense: multiple issues: Description should not be lower case, invalid type for Motion and Battery
239         // Shelly Sense: Battery is reported with Desc "battery", but type "H" instead of "B"
240         // Shelly Sense: Motion is reported with Desc "battery", but type "H" instead of "B"
241         // Shelly Bulb: Colors are coded with Type="Red" etc. rather than Type="S" and color as Descr
242         // Shelly RGBW2 is reporting Brightness, Power, VSwitch for each channel, but all with L=0
243         if (sen.desc == null) {
244             sen.desc = "";
245         }
246         String desc = sen.desc.toLowerCase();
247
248         // RGBW2 reports Power_0, Power_1, Power_2, Power_3; same for VSwitch and Brightness, all of them linkted to L:0
249         // we break it up to Power with L:0, Power with L:1...
250         if (desc.contains("_") && (desc.contains("power") || desc.contains("vswitch") || desc.contains("brightness"))) {
251             String newDesc = substringBefore(sen.desc, "_");
252             String newLink = substringAfter(sen.desc, "_");
253             sen.desc = newDesc;
254             sen.links = newLink;
255             if (!blkMap.containsKey(sen.links)) {
256                 // auto-insert a matching blk entry
257                 CoIotDescrBlk blk = new CoIotDescrBlk();
258                 CoIotDescrBlk blk0 = blkMap.get("0"); // blk 0 is always there
259                 blk.id = sen.links;
260                 blk.desc = blk0.desc + "_" + blk.id;
261                 blkMap.put(blk.id, blk);
262             }
263         }
264
265         switch (sen.type.toLowerCase()) {
266             case "w": // old devices/firmware releases use "W", new ones "P"
267                 sen.type = "P";
268                 sen.desc = "Power";
269                 break;
270             case "tc":
271                 sen.type = "T";
272                 sen.desc = "Temperature C";
273                 break;
274             case "tf":
275                 sen.type = "T";
276                 sen.desc = "Temperature F";
277                 break;
278             case "overtemp":
279                 sen.type = "S";
280                 sen.desc = "Overtemp";
281                 break;
282             case "relay0":
283             case "switch":
284             case "vswitch":
285                 sen.type = "S";
286                 sen.desc = "State";
287                 break;
288         }
289
290         switch (sen.desc.toLowerCase()) {
291             case "motion": // fix acc to spec it's T=M
292                 sen.type = "M";
293                 sen.desc = "Motion";
294                 break;
295             case "battery": // fix: type is B not H
296                 sen.type = "B";
297                 sen.desc = "Battery";
298                 break;
299             case "overtemp":
300                 sen.type = "S";
301                 sen.desc = "Overtemp";
302                 break;
303             case "relay0":
304             case "switch":
305             case "vswitch":
306                 sen.type = "S";
307                 sen.desc = "State";
308                 break;
309             case "e cnt 0 [w-min]": // 4 Pro
310             case "e cnt 1 [w-min]":
311             case "e cnt 2 [w-min]":
312             case "e cnt total [w-min]": // 4 Pro
313                 sen.desc = sen.desc.toLowerCase().replace("e cnt", "energy counter");
314                 break;
315
316         }
317
318         if (sen.desc.isEmpty()) {
319             switch (sen.type.toLowerCase()) {
320                 case "p":
321                     sen.desc = "Power";
322                     break;
323                 case "T":
324                     sen.desc = "Temperature";
325                     break;
326                 case "input":
327                     sen.type = "S";
328                     sen.desc = "Input";
329                     break;
330                 case "output":
331                     sen.type = "S";
332                     sen.desc = "Output";
333                     break;
334                 case "brightness":
335                     sen.type = "S";
336                     sen.desc = "Brightness";
337                     break;
338                 case "red":
339                 case "green":
340                 case "blue":
341                 case "white":
342                 case "gain":
343                 case "temp": // Bulb: Color temperature
344                     sen.desc = sen.type;
345                     sen.type = "S";
346                     break;
347                 case "vswitch":
348                     // it seems that Shelly tends to break their own spec: T is the description and D is no longer
349                     // included -> map D to sen.T and set CatchAll for T
350                     sen.desc = sen.type;
351                     sen.type = "S";
352                     break;
353                 // Default: set no description
354                 // (there are no T values defined in the CoIoT spec)
355                 case "tostate":
356                 default:
357                     sen.desc = "";
358             }
359         }
360         return sen;
361     }
362 }