2 * Copyright (c) 2010-2021 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.shelly.internal.handler;
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.*;
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;
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
39 * @author Markus Michels - Initial contribution
42 public class ShellyComponents {
45 * Update device status
47 * @param th Thing Handler instance
48 * @param profile ShellyDeviceProfile
50 public static boolean updateDeviceStatus(ShellyBaseHandler thingHandler, ShellySettingsStatus status) {
51 if (!thingHandler.areChannelsCreated()) {
52 thingHandler.updateChannelDefinitions(ShellyChannelDefinitions.createDeviceChannels(thingHandler.getThing(),
53 thingHandler.getProfile(), status));
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));
67 thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_SLEEPTIME,
68 toQuantityType(getInteger(status.sleepTime), Units.SECOND));
70 thingHandler.updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_UPDATE, getOnOff(status.hasUpdate));
72 return false; // device status never triggers update
76 * Update Meter channel
78 * @param th Thing Handler instance
79 * @param profile ShellyDeviceProfile
80 * @param status Last ShellySettingsStatus
82 public static boolean updateMeters(ShellyBaseHandler thingHandler, ShellySettingsStatus status) {
83 ShellyDeviceProfile profile = thingHandler.getProfile();
85 double accumulatedWatts = 0.0;
86 double accumulatedTotal = 0.0;
87 double accumulatedReturned = 0.0;
89 boolean updated = false;
90 // Devices without power meters get no updates
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-");
99 // In Relay mode we map eacher meter to the matching channel group
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();
110 groupName = CHANNEL_GROUP_METER;
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));
121 updated |= thingHandler.updateChannel(groupName, CHANNEL_METER_CURRENTWATTS,
122 toQuantityType(getDouble(meter.power), DIGITS_WATT, Units.WATT));
123 accumulatedWatts += getDouble(meter.power);
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;
132 if (meter.counters != null) {
133 updated |= thingHandler.updateChannel(groupName, CHANNEL_METER_LASTMIN1,
134 toQuantityType(getDouble(meter.counters[0]), DIGITS_WATT, Units.WATT));
136 thingHandler.updateChannel(groupName, CHANNEL_LAST_UPDATE,
137 getTimestamp(getString(profile.settings.timezone), getLong(meter.timestamp)));
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));
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));
164 if (emeter.current != null) {
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));
172 accumulatedWatts += getDouble(emeter.power);
173 accumulatedTotal += getDouble(emeter.total) / 1000;
174 accumulatedReturned += getDouble(emeter.totalReturned) / 1000;
176 thingHandler.updateChannel(groupName, CHANNEL_LAST_UPDATE, getTimestamp());
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;
189 String groupName = CHANNEL_GROUP_METER;
190 for (ShellySettingsMeter meter : status.meters) {
192 currentWatts += getDouble(meter.power);
193 totalWatts += getDouble(meter.total);
194 if (meter.counters != null) {
195 lastMin1 += getDouble(meter.counters[0]);
197 if (getLong(meter.timestamp) > timestamp) {
198 timestamp = getLong(meter.timestamp); // newest one
202 // Create channels for 1 Meter
203 if (!thingHandler.areChannelsCreated()) {
204 thingHandler.updateChannelDefinitions(ShellyChannelDefinitions
205 .createMeterChannels(thingHandler.getThing(), status.meters.get(0), groupName));
208 updated |= thingHandler.updateChannel(groupName, CHANNEL_METER_LASTMIN1,
209 toQuantityType(getDouble(lastMin1), DIGITS_WATT, Units.WATT));
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));
218 if (updated && timestamp > 0) {
219 thingHandler.updateChannel(groupName, CHANNEL_LAST_UPDATE,
220 getTimestamp(getString(profile.settings.timezone), timestamp));
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));
238 * Update Sensor channel
240 * @param th Thing Handler instance
241 * @param profile ShellyDeviceProfile
242 * @param status Last ShellySettingsStatus
244 * @throws ShellyApiException
246 public static boolean updateSensors(ShellyBaseHandler thingHandler, ShellySettingsStatus status)
247 throws ShellyApiException {
248 ShellyDeviceProfile profile = thingHandler.getProfile();
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));
259 updated |= thingHandler.updateWakeupReason(sdata.actReasons);
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));
269 thingHandler.postEvent(getString(sdata.sensorError), true);
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();
282 updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_TEMP,
283 toQuantityType(temp.doubleValue(), DIGITS_TEMP, SIUnits.CELSIUS));
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));
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));
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 updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_VIBRATION,
304 getInteger(sdata.accel.vibration) == 1 ? OnOffType.ON : OnOffType.OFF);
306 if (sdata.flood != null) {
307 updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_FLOOD,
308 getOnOff(sdata.flood));
310 if (sdata.smoke != null) {
311 updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_SMOKE,
312 getOnOff(sdata.smoke));
314 if (sdata.gasSensor != null) {
315 updated |= thingHandler.updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_SELFTTEST,
316 getStringType(sdata.gasSensor.selfTestState));
317 updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_ALARM_STATE,
318 getStringType(sdata.gasSensor.alarmState));
319 updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_SSTATE,
320 getStringType(sdata.gasSensor.sensorState));
322 if ((sdata.concentration != null) && sdata.concentration.isValid) {
323 updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_PPM,
324 getDecimal(sdata.concentration.ppm));
326 if ((sdata.adcs != null) && (sdata.adcs.size() > 0)) {
327 ShellyADC adc = sdata.adcs.get(0);
328 updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_VOLTAGE,
329 getDecimal(adc.voltage));
332 boolean charger = (getInteger(profile.settings.externalPower) == 1) || getBool(sdata.charger);
333 if ((profile.settings.externalPower != null) || (sdata.charger != null)) {
334 updated |= thingHandler.updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_CHARGER,
335 charger ? OnOffType.ON : OnOffType.OFF);
337 if (sdata.bat != null) { // no update for Sense
338 // Shelly HT has external_power under settings, Sense and Motion charger under status
339 if (!charger || !profile.isHT) {
340 updated |= thingHandler.updateChannel(CHANNEL_GROUP_BATTERY, CHANNEL_SENSOR_BAT_LEVEL,
341 toQuantityType(getDouble(sdata.bat.value), 0, Units.PERCENT));
343 updated |= thingHandler.updateChannel(CHANNEL_GROUP_BATTERY, CHANNEL_SENSOR_BAT_LEVEL,
346 boolean changed = thingHandler.updateChannel(CHANNEL_GROUP_BATTERY, CHANNEL_SENSOR_BAT_LOW,
347 getDouble(sdata.bat.value) < thingHandler.config.lowBattery ? OnOffType.ON : OnOffType.OFF);
349 if (changed && getDouble(sdata.bat.value) < thingHandler.config.lowBattery) {
350 thingHandler.postEvent(ALARM_TYPE_LOW_BATTERY, false);
354 if (sdata.motion != null) { // Shelly Sense
355 updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_MOTION,
356 getOnOff(sdata.motion));
358 if (sdata.sensor != null) { // Shelly Motion
359 updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_MOTION_ACT,
360 getOnOff(sdata.sensor.motionActive));
361 updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_MOTION,
362 getOnOff(sdata.sensor.motion));
363 long timestamp = getLong(sdata.sensor.motionTimestamp);
364 if (timestamp != 0) {
365 updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_MOTION_TS,
366 getTimestamp(getString(profile.settings.timezone), timestamp));
368 updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_VIBRATION,
369 getOnOff(sdata.sensor.vibration));
372 updated |= thingHandler.updateInputs(status);
375 thingHandler.updateChannel(profile.getControlGroup(0), CHANNEL_LAST_UPDATE, getTimestamp());