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