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