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