]> git.basschouten.com Git - openhab-addons.git/blob
7fff96aae96c2869a7b72da4e3e3cd7695f824a0
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2021 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.handler;
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 org.eclipse.jdt.annotation.NonNullByDefault;
20 import org.openhab.binding.shelly.internal.api.ShellyApiException;
21 import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsEMeter;
22 import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsMeter;
23 import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsStatus;
24 import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellyStatusSensor;
25 import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellyStatusSensor.ShellyADC;
26 import org.openhab.binding.shelly.internal.api.ShellyDeviceProfile;
27 import org.openhab.binding.shelly.internal.provider.ShellyChannelDefinitions;
28 import org.openhab.core.library.types.OnOffType;
29 import org.openhab.core.library.types.OpenClosedType;
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.UnDefType;
34
35 /***
36  * The{@link ShellyComponents} implements updates for supplemental components
37  * Meter will be used by Relay + Light; Sensor is part of H&T, Flood, Door Window, Sense
38  *
39  * @author Markus Michels - Initial contribution
40  */
41 @NonNullByDefault
42 public class ShellyComponents {
43
44     /**
45      * Update device status
46      *
47      * @param th Thing Handler instance
48      * @param profile ShellyDeviceProfile
49      */
50     public static boolean updateDeviceStatus(ShellyBaseHandler thingHandler, ShellySettingsStatus status) {
51         if (!thingHandler.areChannelsCreated()) {
52             thingHandler.updateChannelDefinitions(ShellyChannelDefinitions.createDeviceChannels(thingHandler.getThing(),
53                     thingHandler.getProfile(), status));
54         }
55
56         Integer rssi = getInteger(status.wifiSta.rssi);
57         thingHandler.updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_UPTIME,
58                 toQuantityType((double) getLong(status.uptime), DIGITS_NONE, Units.SECOND));
59         thingHandler.updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_RSSI, mapSignalStrength(rssi));
60         if ((status.tmp != null) && !thingHandler.getProfile().isSensor) {
61             thingHandler.updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_ITEMP,
62                     toQuantityType(getDouble(status.tmp.tC), DIGITS_NONE, SIUnits.CELSIUS));
63         } else if (status.temperature != null) {
64             thingHandler.updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_ITEMP,
65                     toQuantityType(getDouble(status.temperature), DIGITS_NONE, SIUnits.CELSIUS));
66         }
67         thingHandler.updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_UPDATE, getOnOff(status.hasUpdate));
68
69         return false; // device status never triggers update
70     }
71
72     /**
73      * Update Meter channel
74      *
75      * @param th Thing Handler instance
76      * @param profile ShellyDeviceProfile
77      * @param status Last ShellySettingsStatus
78      */
79     public static boolean updateMeters(ShellyBaseHandler thingHandler, ShellySettingsStatus status) {
80         ShellyDeviceProfile profile = thingHandler.getProfile();
81
82         double accumulatedWatts = 0.0;
83         double accumulatedTotal = 0.0;
84         double accumulatedReturned = 0.0;
85
86         boolean updated = false;
87         // Devices without power meters get no updates
88         // We need to differ
89         // Roler+RGBW2 have multiple meters -> aggregate consumption to the functional device
90         // Meter and EMeter have a different set of channels
91         if ((profile.numMeters > 0) && ((status.meters != null) || (status.emeters != null))) {
92             if (!profile.isRoller && !profile.isRGBW2) {
93                 thingHandler.logger.trace("{}: Updating {} {}meter(s)", thingHandler.thingName, profile.numMeters,
94                         !profile.isEMeter ? "standard " : "e-");
95
96                 // In Relay mode we map eacher meter to the matching channel group
97                 int m = 0;
98                 if (!profile.isEMeter) {
99                     for (ShellySettingsMeter meter : status.meters) {
100                         Integer meterIndex = m + 1;
101                         if (getBool(meter.isValid) || profile.isLight) { // RGBW2-white doesn't report valid flag
102                                                                          // correctly in white mode
103                             String groupName = "";
104                             if (profile.numMeters > 1) {
105                                 groupName = CHANNEL_GROUP_METER + meterIndex.toString();
106                             } else {
107                                 groupName = CHANNEL_GROUP_METER;
108                             }
109
110                             if (!thingHandler.areChannelsCreated()) {
111                                 // skip for Shelly Bulb: JSON has a meter, but values don't get updated
112                                 if (!profile.isBulb) {
113                                     thingHandler.updateChannelDefinitions(ShellyChannelDefinitions
114                                             .createMeterChannels(thingHandler.getThing(), meter, groupName));
115                                 }
116                             }
117
118                             updated |= thingHandler.updateChannel(groupName, CHANNEL_METER_CURRENTWATTS,
119                                     toQuantityType(getDouble(meter.power), DIGITS_WATT, Units.WATT));
120                             accumulatedWatts += getDouble(meter.power);
121
122                             // convert Watt/Min to kw/h
123                             if (meter.total != null) {
124                                 double kwh = getDouble(meter.total) / 60 / 1000;
125                                 updated |= thingHandler.updateChannel(groupName, CHANNEL_METER_TOTALKWH,
126                                         toQuantityType(kwh, DIGITS_KWH, Units.KILOWATT_HOUR));
127                                 accumulatedTotal += kwh;
128                             }
129                             if (meter.counters != null) {
130                                 updated |= thingHandler.updateChannel(groupName, CHANNEL_METER_LASTMIN1,
131                                         toQuantityType(getDouble(meter.counters[0]), DIGITS_WATT, Units.WATT));
132                             }
133                             thingHandler.updateChannel(groupName, CHANNEL_LAST_UPDATE,
134                                     getTimestamp(getString(profile.settings.timezone), getLong(meter.timestamp)));
135                         }
136                         m++;
137                     }
138                 } else {
139                     for (ShellySettingsEMeter emeter : status.emeters) {
140                         Integer meterIndex = m + 1;
141                         if (getBool(emeter.isValid)) {
142                             String groupName = profile.numMeters > 1 ? CHANNEL_GROUP_METER + meterIndex.toString()
143                                     : CHANNEL_GROUP_METER;
144                             if (!thingHandler.areChannelsCreated()) {
145                                 thingHandler.updateChannelDefinitions(ShellyChannelDefinitions
146                                         .createEMeterChannels(thingHandler.getThing(), emeter, groupName));
147                             }
148
149                             // convert Watt/Hour tok w/h
150                             updated |= thingHandler.updateChannel(groupName, CHANNEL_METER_CURRENTWATTS,
151                                     toQuantityType(getDouble(emeter.power), DIGITS_WATT, Units.WATT));
152                             updated |= thingHandler.updateChannel(groupName, CHANNEL_METER_TOTALKWH,
153                                     toQuantityType(getDouble(emeter.total) / 1000, DIGITS_KWH, Units.KILOWATT_HOUR));
154                             updated |= thingHandler.updateChannel(groupName, CHANNEL_EMETER_TOTALRET, toQuantityType(
155                                     getDouble(emeter.totalReturned) / 1000, DIGITS_KWH, Units.KILOWATT_HOUR));
156                             updated |= thingHandler.updateChannel(groupName, CHANNEL_EMETER_REACTWATTS,
157                                     toQuantityType(getDouble(emeter.reactive), DIGITS_WATT, Units.WATT));
158                             updated |= thingHandler.updateChannel(groupName, CHANNEL_EMETER_VOLTAGE,
159                                     toQuantityType(getDouble(emeter.voltage), DIGITS_VOLT, Units.VOLT));
160
161                             if (emeter.current != null) {
162                                 // Shelly
163                                 updated |= thingHandler.updateChannel(groupName, CHANNEL_EMETER_CURRENT,
164                                         toQuantityType(getDouble(emeter.current), DIGITS_VOLT, Units.AMPERE));
165                                 updated |= thingHandler.updateChannel(groupName, CHANNEL_EMETER_PFACTOR,
166                                         getDecimal(emeter.pf));
167                             }
168
169                             accumulatedWatts += getDouble(emeter.power);
170                             accumulatedTotal += getDouble(emeter.total) / 1000;
171                             accumulatedReturned += getDouble(emeter.totalReturned) / 1000;
172                             if (updated) {
173                                 thingHandler.updateChannel(groupName, CHANNEL_LAST_UPDATE, getTimestamp());
174                             }
175                         }
176                         m++;
177                     }
178                 }
179             } else {
180                 // In Roller Mode we accumulate all meters to a single set of meters
181                 thingHandler.logger.trace("{}: Updating Meter (accumulated)", thingHandler.thingName);
182                 double currentWatts = 0.0;
183                 double totalWatts = 0.0;
184                 double lastMin1 = 0.0;
185                 long timestamp = 0l;
186                 String groupName = CHANNEL_GROUP_METER;
187                 for (ShellySettingsMeter meter : status.meters) {
188                     if (meter.isValid) {
189                         currentWatts += getDouble(meter.power);
190                         totalWatts += getDouble(meter.total);
191                         if (meter.counters != null) {
192                             lastMin1 += getDouble(meter.counters[0]);
193                         }
194                         if (getLong(meter.timestamp) > timestamp) {
195                             timestamp = getLong(meter.timestamp); // newest one
196                         }
197                     }
198                 }
199                 // Create channels for 1 Meter
200                 if (!thingHandler.areChannelsCreated()) {
201                     thingHandler.updateChannelDefinitions(ShellyChannelDefinitions
202                             .createMeterChannels(thingHandler.getThing(), status.meters.get(0), groupName));
203                 }
204
205                 updated |= thingHandler.updateChannel(groupName, CHANNEL_METER_LASTMIN1,
206                         toQuantityType(getDouble(lastMin1), DIGITS_WATT, Units.WATT));
207
208                 // convert totalWatts into kw/h
209                 totalWatts = totalWatts / (60.0 * 1000.0);
210                 updated |= thingHandler.updateChannel(groupName, CHANNEL_METER_CURRENTWATTS,
211                         toQuantityType(getDouble(currentWatts), DIGITS_WATT, Units.WATT));
212                 updated |= thingHandler.updateChannel(groupName, CHANNEL_METER_TOTALKWH,
213                         toQuantityType(getDouble(totalWatts), DIGITS_KWH, Units.KILOWATT_HOUR));
214
215                 if (updated) {
216                     thingHandler.updateChannel(groupName, CHANNEL_LAST_UPDATE,
217                             getTimestamp(getString(profile.settings.timezone), timestamp));
218                 }
219             }
220
221             if (!profile.isRoller && !profile.isRGBW2) {
222                 thingHandler.updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_ACCUWATTS,
223                         toQuantityType(accumulatedWatts, DIGITS_WATT, Units.WATT));
224                 thingHandler.updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_ACCUTOTAL,
225                         toQuantityType(accumulatedTotal, DIGITS_KWH, Units.KILOWATT_HOUR));
226                 thingHandler.updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_ACCURETURNED,
227                         toQuantityType(accumulatedReturned, DIGITS_KWH, Units.KILOWATT_HOUR));
228             }
229         }
230
231         return updated;
232     }
233
234     /**
235      * Update Sensor channel
236      *
237      * @param th Thing Handler instance
238      * @param profile ShellyDeviceProfile
239      * @param status Last ShellySettingsStatus
240      *
241      * @throws ShellyApiException
242      */
243     public static boolean updateSensors(ShellyBaseHandler thingHandler, ShellySettingsStatus status)
244             throws ShellyApiException {
245         ShellyDeviceProfile profile = thingHandler.getProfile();
246
247         boolean updated = false;
248         if (profile.isSensor || profile.hasBattery) {
249             ShellyStatusSensor sdata = thingHandler.api.getSensorStatus();
250
251             if (!thingHandler.areChannelsCreated()) {
252                 thingHandler.logger.trace("{}: Create missing sensor channel(s)", thingHandler.thingName);
253                 thingHandler.updateChannelDefinitions(
254                         ShellyChannelDefinitions.createSensorChannels(thingHandler.getThing(), profile, sdata));
255             }
256
257             updated |= thingHandler.updateWakeupReason(sdata.actReasons);
258
259             if ((sdata.sensor != null) && sdata.sensor.isValid) {
260                 // Shelly DW: “sensor”:{“state”:“open”, “is_valid”:true},
261                 updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_CONTACT,
262                         getString(sdata.sensor.state).equalsIgnoreCase(SHELLY_API_DWSTATE_OPEN) ? OpenClosedType.OPEN
263                                 : OpenClosedType.CLOSED);
264                 String sensorError = getString(sdata.sensorError);
265                 boolean changed = thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_ERROR,
266                         getStringType(sdata.sensorError));
267                 if (changed) {
268                     thingHandler.postEvent(getString(sdata.sensorError), true);
269                 }
270                 updated |= changed;
271             }
272             if ((sdata.tmp != null) && getBool(sdata.tmp.isValid)) {
273                 thingHandler.logger.trace("{}: Updating temperature", thingHandler.thingName);
274                 Double temp = getString(sdata.tmp.units).toUpperCase().equals(SHELLY_TEMP_CELSIUS)
275                         ? getDouble(sdata.tmp.tC)
276                         : getDouble(sdata.tmp.tF);
277                 if (getString(sdata.tmp.units).toUpperCase().equals(SHELLY_TEMP_FAHRENHEIT)) {
278                     // convert Fahrenheit to Celsius
279                     temp = ImperialUnits.FAHRENHEIT.getConverterTo(SIUnits.CELSIUS).convert(temp).doubleValue();
280                 }
281                 updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_TEMP,
282                         toQuantityType(temp.doubleValue(), DIGITS_TEMP, SIUnits.CELSIUS));
283             }
284             if (sdata.hum != null) {
285                 thingHandler.logger.trace("{}: Updating humidity", thingHandler.thingName);
286                 updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_HUM,
287                         toQuantityType(getDouble(sdata.hum.value), DIGITS_PERCENT, Units.PERCENT));
288             }
289             if ((sdata.lux != null) && getBool(sdata.lux.isValid)) {
290                 // “lux”:{“value”:30, “illumination”: “dark”, “is_valid”:true},
291                 thingHandler.logger.trace("{}: Updating lux", thingHandler.thingName);
292                 updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_LUX,
293                         toQuantityType(getDouble(sdata.lux.value), DIGITS_LUX, Units.LUX));
294                 if (sdata.lux.illumination != null) {
295                     updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_ILLUM,
296                             getStringType(sdata.lux.illumination));
297                 }
298             }
299             if (sdata.accel != null) {
300                 updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_TILT,
301                         toQuantityType(getDouble(sdata.accel.tilt.doubleValue()), DIGITS_NONE, Units.DEGREE_ANGLE));
302                 updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_VIBRATION,
303                         getInteger(sdata.accel.vibration) == 1 ? OnOffType.ON : OnOffType.OFF);
304             }
305             if (sdata.flood != null) {
306                 updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_FLOOD,
307                         getOnOff(sdata.flood));
308             }
309             if (sdata.smoke != null) {
310                 updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_SMOKE,
311                         getOnOff(sdata.smoke));
312             }
313             if (sdata.gasSensor != null) {
314                 updated |= thingHandler.updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_SELFTTEST,
315                         getStringType(sdata.gasSensor.selfTestState));
316                 updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_ALARM_STATE,
317                         getStringType(sdata.gasSensor.alarmState));
318                 updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_SSTATE,
319                         getStringType(sdata.gasSensor.sensorState));
320             }
321             if ((sdata.concentration != null) && sdata.concentration.isValid) {
322                 updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_PPM,
323                         getDecimal(sdata.concentration.ppm));
324             }
325             if ((sdata.adcs != null) && (sdata.adcs.size() > 0)) {
326                 ShellyADC adc = sdata.adcs.get(0);
327                 updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_VOLTAGE,
328                         getDecimal(adc.voltage));
329             }
330
331             boolean charger = (getInteger(profile.settings.externalPower) == 1) || getBool(sdata.charger);
332             if ((profile.settings.externalPower != null) || (sdata.charger != null)) {
333                 updated |= thingHandler.updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_CHARGER,
334                         charger ? OnOffType.ON : OnOffType.OFF);
335             }
336             if (sdata.bat != null) { // no update for Sense
337                 // Shelly HT has external_power under settings, Sense and Motion charger under status
338                 if (!charger || !profile.isHT) {
339                     updated |= thingHandler.updateChannel(CHANNEL_GROUP_BATTERY, CHANNEL_SENSOR_BAT_LEVEL,
340                             toQuantityType(getDouble(sdata.bat.value), 0, Units.PERCENT));
341                 } else {
342                     updated |= thingHandler.updateChannel(CHANNEL_GROUP_BATTERY, CHANNEL_SENSOR_BAT_LEVEL,
343                             UnDefType.UNDEF);
344                 }
345                 boolean changed = thingHandler.updateChannel(CHANNEL_GROUP_BATTERY, CHANNEL_SENSOR_BAT_LOW,
346                         getDouble(sdata.bat.value) < thingHandler.config.lowBattery ? OnOffType.ON : OnOffType.OFF);
347                 updated |= changed;
348                 if (changed && getDouble(sdata.bat.value) < thingHandler.config.lowBattery) {
349                     thingHandler.postEvent(ALARM_TYPE_LOW_BATTERY, false);
350                 }
351             }
352
353             if (sdata.motion != null) { // Shelly Sense
354                 updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_MOTION,
355                         getOnOff(sdata.motion));
356             }
357             if (sdata.sensor != null) { // Shelly Motion
358                 updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_MOTION,
359                         getOnOff(sdata.sensor.motion));
360                 long timestamp = getLong(sdata.sensor.motionTimestamp);
361                 if (timestamp != 0) {
362                     updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_MOTION_TS,
363                             getTimestamp(getString(profile.settings.timezone), timestamp));
364                 }
365                 updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_VIBRATION,
366                         getOnOff(sdata.sensor.vibration));
367             }
368
369             updated |= thingHandler.updateInputs(status);
370
371             if (updated) {
372                 thingHandler.updateChannel(profile.getControlGroup(0), CHANNEL_LAST_UPDATE, getTimestamp());
373             }
374         }
375         return updated;
376     }
377 }