]> git.basschouten.com Git - openhab-addons.git/blob
3b7f410697db230ff6c038517ee97a4b27d755d5
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2022 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 /**
14  * The {@link ShellyCoIoTVersion1} implements the parsing for CoIoT version 1
15  *
16  * @author Markus Michels - Initial contribution
17  */
18 package org.openhab.binding.shelly.internal.coap;
19
20 import static org.openhab.binding.shelly.internal.ShellyBindingConstants.*;
21 import static org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.*;
22 import static org.openhab.binding.shelly.internal.util.ShellyUtils.*;
23
24 import java.util.List;
25 import java.util.Map;
26
27 import org.eclipse.jdt.annotation.NonNullByDefault;
28 import org.eclipse.jdt.annotation.Nullable;
29 import org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO.CoIotDescrBlk;
30 import org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO.CoIotDescrSen;
31 import org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO.CoIotSensor;
32 import org.openhab.binding.shelly.internal.handler.ShellyBaseHandler;
33 import org.openhab.binding.shelly.internal.handler.ShellyColorUtils;
34 import org.openhab.core.library.types.OnOffType;
35 import org.openhab.core.library.types.OpenClosedType;
36 import org.openhab.core.library.unit.SIUnits;
37 import org.openhab.core.library.unit.Units;
38 import org.openhab.core.types.State;
39 import org.openhab.core.types.UnDefType;
40 import org.slf4j.Logger;
41 import org.slf4j.LoggerFactory;
42
43 /**
44  * The {@link ShellyCoIoTVersion1} implements the parsing for CoIoT version 2
45  *
46  * @author Markus Michels - Initial contribution
47  */
48 @NonNullByDefault
49 public class ShellyCoIoTVersion2 extends ShellyCoIoTProtocol implements ShellyCoIoTInterface {
50     private final Logger logger = LoggerFactory.getLogger(ShellyCoIoTVersion2.class);
51
52     public ShellyCoIoTVersion2(String thingName, ShellyBaseHandler thingHandler, Map<String, CoIotDescrBlk> blkMap,
53             Map<String, CoIotDescrSen> sensorMap) {
54         super(thingName, thingHandler, blkMap, sensorMap);
55     }
56
57     @Override
58     public int getVersion() {
59         return ShellyCoapJSonDTO.COIOT_VERSION_2;
60     }
61
62     /**
63      * Process CoIoT status update message. If a status update is received, but the device description has not been
64      * received yet a GET is send to query device description.
65      *
66      * @param sensorUpdates Complete list of sensor updates
67      * @param sen The specific sensor update to handle
68      * @param updates Resulting updates (new updates will be added to input list)
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         int rIndex = getIdFromBlk(sen);
82         String rGroup = getProfile().numRelays <= 1 ? CHANNEL_GROUP_RELAY_CONTROL
83                 : CHANNEL_GROUP_RELAY_CONTROL + rIndex;
84         String mGroup = profile.numMeters <= 1 ? CHANNEL_GROUP_METER
85                 : CHANNEL_GROUP_METER + (profile.isEMeter ? getIdFromBlk(sen) : rIndex);
86
87         boolean processed = true;
88         double value = getDouble(s.value);
89         String reason = "";
90
91         if (profile.isTRV) {
92             // Special handling for TRV, because it uses duplicate ID values with different meanings
93             switch (sen.id) {
94                 case "3101": // current temp
95                     updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_TEMP,
96                             toQuantityType(value, DIGITS_TEMP, SIUnits.CELSIUS));
97                     break;
98                 case "3103": // target temp in C. 4/31, 999=unknown
99                     updateChannel(updates, CHANNEL_GROUP_CONTROL, CHANNEL_CONTROL_SETTEMP,
100                             toQuantityType(value, DIGITS_TEMP, SIUnits.CELSIUS));
101                     break;
102                 case "3116": // S, valveError, 0/1
103                     if (s.value == 1) {
104                         thingHandler.postEvent(ALARM_TYPE_VALVE_ERROR, false);
105                     }
106                     break;
107                 case "3117": // S, mode, 0-5 (0=disabled)
108                     value = getDouble(s.value).intValue();
109                     updateChannel(updates, CHANNEL_GROUP_CONTROL, CHANNEL_CONTROL_PROFILE, getDecimal(value));
110                     updateChannel(updates, CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_SCHEDULE, getOnOff(value > 0));
111                     break;
112                 case "3118": // Valve state
113                     updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_STATE,
114                             value != 0 ? OpenClosedType.OPEN : OpenClosedType.CLOSED);
115                     break;
116                 case "3121": // valvePos, Type=S, Range=0/100;
117                     updateChannel(updates, CHANNEL_GROUP_CONTROL, CHANNEL_CONTROL_POSITION,
118                             s.value != -1 ? toQuantityType(getDouble(s.value), 0, Units.PERCENT) : UnDefType.UNDEF);
119                     break;
120                 case "3122": // boostMinutes
121                     updateChannel(updates, CHANNEL_GROUP_CONTROL, CHANNEL_CONTROL_BTIMER,
122                             s.value != -1 ? toQuantityType(s.value, DIGITS_NONE, Units.MINUTE) : UnDefType.UNDEF);
123                     break;
124                 default:
125                     processed = false;
126             }
127         } else {
128             processed = false;
129         }
130
131         if (processed) {
132             return true;
133         }
134
135         processed = true;
136         switch (sen.id) {
137             case "3106": // L, luminosity, lux, U32, -1
138             case "3110": // S, luminosityLevel, dark/twilight/bright, "unknown"=unknown
139             case "3111": // B, battery, 0-100%, unknown -1
140             case "3112": // S, charger, 0/1
141             case "3115": // S, sensorError, 0/1
142                 // processed by base handler
143                 break;
144
145             case "6109": // P, overpowerValue, W, U32
146             case "9101":
147                 // Relay: S, mode, relay/roller or
148                 // Dimmer: S, mode, color/white
149                 // skip, could check against thing mode...
150                 break;
151
152             case "1101": // relay_0: output, 0/1
153             case "1201": // relay_1: output, 0/1
154             case "1301": // relay_2: output, 0/1
155             case "1401": // relay_3: output, 0/1
156                 updatePower(profile, updates, rIndex, sen, s, sensorUpdates);
157                 break;
158             case "1102": // roler_0: S, roller, open/close/stop -> roller state
159                 updateChannel(updates, CHANNEL_GROUP_ROL_CONTROL, CHANNEL_ROL_CONTROL_STATE, getStringType(s.valueStr));
160                 break;
161             case "1103": // roller_0: S, rollerPos, 0-100, unknown -1
162                 int pos = Math.max(SHELLY_MIN_ROLLER_POS, Math.min((int) value, SHELLY_MAX_ROLLER_POS));
163                 logger.debug("{}: CoAP update roller position: control={}, position={}", thingName,
164                         SHELLY_MAX_ROLLER_POS - pos, pos);
165                 updateChannel(updates, CHANNEL_GROUP_ROL_CONTROL, CHANNEL_ROL_CONTROL_CONTROL,
166                         toQuantityType((double) (SHELLY_MAX_ROLLER_POS - pos), Units.PERCENT));
167                 updateChannel(updates, CHANNEL_GROUP_ROL_CONTROL, CHANNEL_ROL_CONTROL_POS,
168                         toQuantityType((double) pos, Units.PERCENT));
169                 break;
170             case "1105": // Gas: S, valve, closed/opened/not_connected/failure/closing/opening/checking or unknown
171                 updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_VALVE, getStringType(s.valueStr));
172                 break;
173
174             case "2101": // Input_0: S, input, 0/1
175             case "2201": // Input_1: S, input, 0/1
176             case "2301": // Input_2: S, input, 0/1
177             case "2401": // Input_3: S, input, 0/1
178                 handleInput(sen, s, rGroup, updates);
179                 break;
180             case "2102": // Input_0: EV, inputEvent, S/SS/SSS/L
181             case "2202": // Input_1: EV, inputEvent
182             case "2302": // Input_2: EV, inputEvent
183             case "2402": // Input_3: EV, inputEvent
184                 handleInputEvent(sen, getString(s.valueStr), -1, serial, updates);
185                 break;
186             case "2103": // EVC, inputEventCnt, U16
187             case "2203": // EVC, inputEventCnt, U16
188             case "2303": // EVC, inputEventCnt, U16
189             case "2403": // EVC, inputEventCnt, U16
190                 handleInputEvent(sen, "", getInteger((int) s.value), serial, updates);
191                 break;
192             case "3101": // sensor_0: T, extTemp, C, -55/125; unknown 999
193             case "3201": // sensor_1: T, extTemp, C, -55/125; unknown 999
194             case "3301": // sensor_2: T, extTemp, C, -55/125; unknown 999
195                 int idx = getExtTempId(sen.id);
196                 if (idx >= 0) {
197                     // H&T, Fllod, DW only have 1 channel, 1/1PM with Addon have up to to 3 sensors
198                     String channel = profile.isSensor ? CHANNEL_SENSOR_TEMP : CHANNEL_SENSOR_TEMP + idx;
199                     // Some devices report values = -999 or 99 during fw update
200                     boolean valid = value > -50 && value < 90;
201                     updateChannel(updates, CHANNEL_GROUP_SENSOR, channel,
202                             valid ? toQuantityType(value, DIGITS_TEMP, SIUnits.CELSIUS) : UnDefType.UNDEF);
203                 } else {
204                     logger.debug("{}: Unable to get extSensorId {} from {}/{}", thingName, sen.id, sen.type, sen.desc);
205                 }
206                 break;
207             case "3104": // T, deviceTemp, Celsius -40/300; 999=unknown
208                 if ("targetTemp".equalsIgnoreCase(sen.desc)) {
209
210                     break; // target temp in F-> ignore
211                 }
212                 // sensor_0: T, internalTemp, F, 39/88, unknown 999
213                 updateChannel(updates, CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_ITEMP,
214                         toQuantityType(value, DIGITS_NONE, SIUnits.CELSIUS));
215                 break;
216             case "3102": // sensor_0: T, extTemp, F, -67/257, unknown 999
217             case "3202": // sensor_1: T, extTemp, F, -67/257, unknown 999
218             case "3302": // sensor_2: T, extTemp, F, -67/257, unknown 999
219             case "3105": // T, deviceTemp, Fahrenheit -40/572
220                 // skip, we use only C
221                 break;
222
223             case "3107": // C, Gas concentration, U16
224                 updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_PPM, getDecimal(s.value));
225                 break;
226             case "3108": // DW: S, dwIsOpened, 0/1, -1=unknown
227                 if (value != -1) {
228                     updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_STATE,
229                             value != 0 ? OpenClosedType.OPEN : OpenClosedType.CLOSED);
230                 } else {
231                     logger.debug("{}: Sensor error reported, check device, battery and installation", thingName);
232                 }
233                 break;
234             case "3109": // S, tilt, 0-180deg, -1
235                 updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_TILT,
236                         toQuantityType(s.value, DIGITS_NONE, Units.DEGREE_ANGLE));
237                 break;
238             case "3113": // S, sensorOp, warmup/normal/fault
239                 updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_SSTATE, getStringType(s.valueStr));
240                 break;
241             case "3114": // S, selfTest, not_completed/completed/running/pending
242                 updateChannel(updates, CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_SELFTTEST, getStringType(s.valueStr));
243                 break;
244             case "3117": // S, extInput, 0/1
245                 handleInput(sen, s, rGroup, updates);
246                 break;
247             case "3118":
248                 updateChannel(updates, mGroup, CHANNEL_SENSOR_VOLTAGE,
249                         toQuantityType(getDouble(s.value), 2, Units.VOLT));
250                 break;
251
252             case "4101": // relay_0/light_0: P, power, W
253             case "4201": // relay_1/light_1: P, power, W
254             case "4301": // relay_2/light_2: P, power, W
255             case "4401": // relay_3/light_3: P, power, W
256             case "4105": // emeter_0: P, power, W
257             case "4205": // emeter_1: P, power, W
258             case "4305": // emeter_2: P, power, W
259             case "4102": // roller_0: P, rollerPower, W, 0-2300, unknown -1
260             case "4202": // roller_1: P, rollerPower, W, 0-2300, unknown -1
261                 logger.debug("{}: Updating {}:currentWatts with {}", thingName, mGroup, s.value);
262                 updateChannel(updates, mGroup, CHANNEL_METER_CURRENTWATTS,
263                         toQuantityType(s.value, DIGITS_WATT, Units.WATT));
264                 if (!profile.isRGBW2 && !profile.isRoller) {
265                     // only for regular, not-aggregated meters
266                     updateChannel(updates, mGroup, CHANNEL_LAST_UPDATE, getTimestamp());
267                 }
268                 break;
269
270             case "4103": // relay_0: E, energy, Wmin, U32
271             case "4203": // relay_1: E, energy, Wmin, U32
272             case "4303": // relay_2: E, energy, Wmin, U32
273             case "4403": // relay_3: E, energy, Wmin, U32
274             case "4104": // roller_0: E, rollerEnergy, Wmin, U32, -1
275             case "4204": // roller_0: E, rollerEnergy, Wmin, U32, -1
276             case "4106": // emeter_0: E, energy, Wh, U32
277             case "4206": // emeter_1: E, energy, Wh, U32
278             case "4306": // emeter_2: E, energy, Wh, U32
279                 double total = profile.isEMeter ? s.value / 1000 : s.value / 60 / 1000;
280                 updateChannel(updates, mGroup, CHANNEL_METER_TOTALKWH,
281                         toQuantityType(total, DIGITS_KWH, Units.KILOWATT_HOUR));
282                 break;
283
284             case "4107": // emeter_0: E, energyReturned, Wh, U32, -1
285             case "4207": // emeter_1: E, energyReturned, Wh, U32, -1
286             case "4307": // emeter_2: E, energyReturned, Wh, U32, -1
287                 updateChannel(updates, mGroup, CHANNEL_EMETER_TOTALRET,
288                         toQuantityType(getDouble(s.value) / 1000, DIGITS_KWH, Units.KILOWATT_HOUR));
289                 break;
290
291             case "4108": // emeter_0: V, voltage, 0-265V, U32, -1
292             case "4208": // emeter_1: V, voltage, 0-265V, U32, -1
293             case "4308": // emeter_2: V, voltage, 0-265V, U32, -1
294                 updateChannel(updates, mGroup, CHANNEL_EMETER_VOLTAGE,
295                         toQuantityType(getDouble(s.value), DIGITS_VOLT, Units.VOLT));
296                 break;
297
298             case "4109": // emeter_0: A, current, 0/120A, -1
299             case "4209": // emeter_1: A, current, 0/120A, -1
300             case "4309": // emeter_2: A, current, 0/120A, -1
301                 updateChannel(updates, rGroup, CHANNEL_EMETER_CURRENT,
302                         toQuantityType(getDouble(s.value), DIGITS_VOLT, Units.AMPERE));
303                 break;
304
305             case "4110": // emeter_0: S, powerFactor, 0/1, -1
306             case "4210": // emeter_1: S, powerFactor, 0/1, -1
307             case "4310": // emeter_2: S, powerFactor, 0/1, -1
308                 updateChannel(updates, rGroup, CHANNEL_EMETER_PFACTOR, getDecimal(s.value));
309                 break;
310
311             case "5101": // {"I":5101,"T":"S","D":"brightness","R":"0/100","L":1},
312             case "5102": // {"I":5102,"T":"S","D":"gain","R":"0/100","L":1},
313             case "5103": // {"I":5103,"T":"S","D":"colorTemp","U":"K","R":"3000/6500","L":1},
314             case "5105": // {"I":5105,"T":"S","D":"red","R":"0/255","L":1},
315             case "5106": // {"I":5106,"T":"S","D":"green","R":"0/255","L":1},
316             case "5107": // {"I":5107,"T":"S","D":"blue","R":"0/255","L":1},
317             case "5108": // {"I":5108,"T":"S","D":"white","R":"0/255","L":1},
318                 // already covered by base handler
319                 break;
320
321             case "6101": // A, overtemp, 0/1
322                 if (s.value == 1) {
323                     thingHandler.postEvent(ALARM_TYPE_OVERTEMP, true);
324                 }
325                 break;
326             case "6102": // relay_0: A, overpower, 0/1
327             case "6202": // relay_1: A, overpower, 0/1
328             case "6302": // relay_2: A, overpower, 0/1
329             case "6402": // relay_3: A, overpower, 0/1
330                 if (s.value == 1) {
331                     thingHandler.postEvent(ALARM_TYPE_OVERPOWER, true);
332                 }
333                 break;
334             case "6104": // relay_0: A, loadError, 0/1
335             case "6204": // relay_1: A, loadError, 0/1
336             case "6304": // relay_2: A, loadError, 0/1
337             case "6404": // relay_3: A, loadError, 0/1
338                 if (s.value == 1) {
339                     thingHandler.postEvent(ALARM_TYPE_LOADERR, true);
340                 }
341                 break;
342             case "6103": // roller_0: A, rollerStopReason, normal/safety_switch/obstacle/overpower
343                 reason = getString(s.valueStr);
344                 updateChannel(updates, CHANNEL_GROUP_ROL_CONTROL, CHANNEL_ROL_CONTROL_STOPR, getStringType(reason));
345                 if (!reason.isEmpty() && !reason.equalsIgnoreCase(SHELLY_API_STOPR_NORMAL)) {
346                     thingHandler.postEvent("ROLLER_" + reason.toUpperCase(), true);
347                 }
348             case "6106": // A, flood, 0/1, -1
349                 updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_FLOOD,
350                         value == 1 ? OnOffType.ON : OnOffType.OFF);
351                 break;
352
353             case "6107": // A, motion, 0/1, -1
354                 // {"I":6107,"T":"A","D":"motion","R":["0/1","-1"],"L":1},
355                 updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_MOTION,
356                         value == 1 ? OnOffType.ON : OnOffType.OFF);
357                 break;
358             case "3119": // Motion timestamp (timestamp os GMT, not adapted to the adapted timezone)
359                 // {"I":3119,"T":"S","D":"timestamp","U":"s","R":["U32","-1"],"L":1},
360                 if (s.value != 0) {
361                     updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_MOTION_TS,
362                             getTimestamp(getString("GMT"), (long) s.value));
363                 }
364                 break;
365             case "3120": // motionActive (timestamp os GMT, not adapted to the adapted timezone)
366                 // {"I":3120,"T":"S","D":"motionActive","R":["0/1","-1"],"L":1},
367                 updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_MOTION_ACT,
368                         getTimestamp("GMT", (long) s.value));
369                 break;
370
371             case "6108": // A, gas, none/mild/heavy/test or unknown
372                 updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_ALARM_STATE, getStringType(s.valueStr));
373                 break;
374             case "6110": // A, vibration, 0/1, -1=unknown
375                 updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_VIBRATION,
376                         s.value == 1 ? OnOffType.ON : OnOffType.OFF);
377                 if (s.value == 1) {
378                     // post event
379                     thingHandler.triggerChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_ALARM, EVENT_TYPE_VIBRATION);
380                 }
381                 break;
382             case "9102": // EV, wakeupEvent, battery/button/periodic/poweron/sensor/ext_power, "unknown"=unknown
383                 if (s.valueArray.size() > 0) {
384                     thingHandler.updateWakeupReason(s.valueArray);
385                     lastWakeup = (String) s.valueArray.get(0);
386                 }
387                 break;
388             case "9103": // EVC, cfgChanged, U16
389                 if ((lastCfgCount != -1) && (lastCfgCount != s.value)) {
390                     thingHandler.requestUpdates(1, true); // refresh config
391                 }
392                 lastCfgCount = (int) s.value;
393                 break;
394
395             default:
396                 processed = false;
397         }
398         return processed;
399     }
400
401     @Override
402     public CoIotDescrSen fixDescription(@Nullable CoIotDescrSen sen, Map<String, CoIotDescrBlk> blkMap) {
403         return super.fixDescription(sen, blkMap);
404     }
405
406     private static final String ID_4101_DESCR = "{ \"I\":4101, \"T\":\"P\", \"D\":\"power\",  \"U\": \"W\",    \"R\":\"0/3500\", \"L\": 1}";
407     private static final String ID_4103_DESCR = "{ \"I\":4103, \"T\":\"E\", \"D\":\"energy\", \"U\": \"Wmin\", \"R\":\"U32\", \"L\": 1}";
408
409     @Override
410     public void completeMissingSensorDefinition(Map<String, CoIotDescrSen> sensorMap) {
411         if (profile.isDuo && profile.inColor) {
412             addSensor(sensorMap, "4101", ID_4101_DESCR);
413             addSensor(sensorMap, "4103", ID_4103_DESCR);
414         }
415         super.completeMissingSensorDefinition(sensorMap);
416     }
417 }