2 * Copyright (c) 2010-2022 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.eclipse.jdt.annotation.Nullable;
21 import org.openhab.binding.shelly.internal.api.ShellyApiException;
22 import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsEMeter;
23 import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsMeter;
24 import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsStatus;
25 import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellyStatusSensor;
26 import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellyStatusSensor.ShellyADC;
27 import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellyThermnostat;
28 import org.openhab.binding.shelly.internal.api.ShellyDeviceProfile;
29 import org.openhab.binding.shelly.internal.provider.ShellyChannelDefinitions;
30 import org.openhab.core.library.types.OnOffType;
31 import org.openhab.core.library.types.OpenClosedType;
32 import org.openhab.core.library.unit.ImperialUnits;
33 import org.openhab.core.library.unit.SIUnits;
34 import org.openhab.core.library.unit.Units;
35 import org.openhab.core.types.UnDefType;
38 * The{@link ShellyComponents} implements updates for supplemental components
39 * Meter will be used by Relay + Light; Sensor is part of H&T, Flood, Door Window, Sense
41 * @author Markus Michels - Initial contribution
44 public class ShellyComponents {
47 * Update device status
49 * @param th Thing Handler instance
50 * @param profile ShellyDeviceProfile
52 public static boolean updateDeviceStatus(ShellyBaseHandler thingHandler, ShellySettingsStatus status) {
53 if (!thingHandler.areChannelsCreated()) {
54 thingHandler.updateChannelDefinitions(ShellyChannelDefinitions.createDeviceChannels(thingHandler.getThing(),
55 thingHandler.getProfile(), status));
58 Integer rssi = getInteger(status.wifiSta.rssi);
59 thingHandler.updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_UPTIME,
60 toQuantityType((double) getLong(status.uptime), DIGITS_NONE, Units.SECOND));
61 thingHandler.updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_RSSI, mapSignalStrength(rssi));
62 if ((status.tmp != null) && !thingHandler.getProfile().isSensor) {
63 thingHandler.updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_ITEMP,
64 toQuantityType(getDouble(status.tmp.tC), DIGITS_NONE, SIUnits.CELSIUS));
65 } else if (status.temperature != null) {
66 thingHandler.updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_ITEMP,
67 toQuantityType(getDouble(status.temperature), DIGITS_NONE, SIUnits.CELSIUS));
69 thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_SLEEPTIME,
70 toQuantityType(getInteger(status.sleepTime), Units.SECOND));
72 thingHandler.updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_UPDATE, getOnOff(status.hasUpdate));
74 ShellyDeviceProfile profile = thingHandler.getProfile();
75 if (profile.settings.calibrated != null) {
76 thingHandler.updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_CALIBRATED,
77 getOnOff(profile.settings.calibrated));
80 return false; // device status never triggers update
84 * Update Meter channel
86 * @param th Thing Handler instance
87 * @param profile ShellyDeviceProfile
88 * @param status Last ShellySettingsStatus
90 public static boolean updateMeters(ShellyBaseHandler thingHandler, ShellySettingsStatus status) {
91 ShellyDeviceProfile profile = thingHandler.getProfile();
93 double accumulatedWatts = 0.0;
94 double accumulatedTotal = 0.0;
95 double accumulatedReturned = 0.0;
97 boolean updated = false;
98 // Devices without power meters get no updates
100 // Roler+RGBW2 have multiple meters -> aggregate consumption to the functional device
101 // Meter and EMeter have a different set of channels
102 if ((profile.numMeters > 0) && ((status.meters != null) || (status.emeters != null))) {
103 if (!profile.isRoller && !profile.isRGBW2) {
104 thingHandler.logger.trace("{}: Updating {} {}meter(s)", thingHandler.thingName, profile.numMeters,
105 !profile.isEMeter ? "standard " : "e-");
107 // In Relay mode we map eacher meter to the matching channel group
109 if (!profile.isEMeter) {
110 for (ShellySettingsMeter meter : status.meters) {
111 Integer meterIndex = m + 1;
112 if (getBool(meter.isValid) || profile.isLight) { // RGBW2-white doesn't report valid flag
113 // correctly in white mode
114 String groupName = "";
115 if (profile.numMeters > 1) {
116 groupName = CHANNEL_GROUP_METER + meterIndex.toString();
118 groupName = CHANNEL_GROUP_METER;
121 if (!thingHandler.areChannelsCreated()) {
122 // skip for Shelly Bulb: JSON has a meter, but values don't get updated
123 if (!profile.isBulb) {
124 thingHandler.updateChannelDefinitions(ShellyChannelDefinitions
125 .createMeterChannels(thingHandler.getThing(), meter, groupName));
129 updated |= thingHandler.updateChannel(groupName, CHANNEL_METER_CURRENTWATTS,
130 toQuantityType(getDouble(meter.power), DIGITS_WATT, Units.WATT));
131 accumulatedWatts += getDouble(meter.power);
133 // convert Watt/Min to kw/h
134 if (meter.total != null) {
135 double kwh = getDouble(meter.total) / 60 / 1000;
136 updated |= thingHandler.updateChannel(groupName, CHANNEL_METER_TOTALKWH,
137 toQuantityType(kwh, DIGITS_KWH, Units.KILOWATT_HOUR));
138 accumulatedTotal += kwh;
140 if (meter.counters != null) {
141 updated |= thingHandler.updateChannel(groupName, CHANNEL_METER_LASTMIN1,
142 toQuantityType(getDouble(meter.counters[0]), DIGITS_WATT, Units.WATT));
144 thingHandler.updateChannel(groupName, CHANNEL_LAST_UPDATE,
145 getTimestamp(getString(profile.settings.timezone), getLong(meter.timestamp)));
150 for (ShellySettingsEMeter emeter : status.emeters) {
151 Integer meterIndex = m + 1;
152 if (getBool(emeter.isValid)) {
153 String groupName = profile.numMeters > 1 ? CHANNEL_GROUP_METER + meterIndex.toString()
154 : CHANNEL_GROUP_METER;
155 if (!thingHandler.areChannelsCreated()) {
156 thingHandler.updateChannelDefinitions(ShellyChannelDefinitions
157 .createEMeterChannels(thingHandler.getThing(), emeter, groupName));
160 // convert Watt/Hour tok w/h
161 updated |= thingHandler.updateChannel(groupName, CHANNEL_METER_CURRENTWATTS,
162 toQuantityType(getDouble(emeter.power), DIGITS_WATT, Units.WATT));
163 updated |= thingHandler.updateChannel(groupName, CHANNEL_METER_TOTALKWH,
164 toQuantityType(getDouble(emeter.total) / 1000, DIGITS_KWH, Units.KILOWATT_HOUR));
165 updated |= thingHandler.updateChannel(groupName, CHANNEL_EMETER_TOTALRET, toQuantityType(
166 getDouble(emeter.totalReturned) / 1000, DIGITS_KWH, Units.KILOWATT_HOUR));
167 updated |= thingHandler.updateChannel(groupName, CHANNEL_EMETER_REACTWATTS,
168 toQuantityType(getDouble(emeter.reactive), DIGITS_WATT, Units.WATT));
169 updated |= thingHandler.updateChannel(groupName, CHANNEL_EMETER_VOLTAGE,
170 toQuantityType(getDouble(emeter.voltage), DIGITS_VOLT, Units.VOLT));
171 updated |= thingHandler.updateChannel(groupName, CHANNEL_EMETER_CURRENT,
172 toQuantityType(getDouble(emeter.current), DIGITS_VOLT, Units.AMPERE));
173 updated |= thingHandler.updateChannel(groupName, CHANNEL_EMETER_PFACTOR,
174 toQuantityType(computePF(emeter), Units.PERCENT));
176 accumulatedWatts += getDouble(emeter.power);
177 accumulatedTotal += getDouble(emeter.total) / 1000;
178 accumulatedReturned += getDouble(emeter.totalReturned) / 1000;
180 thingHandler.updateChannel(groupName, CHANNEL_LAST_UPDATE, getTimestamp());
187 // In Roller Mode we accumulate all meters to a single set of meters
188 thingHandler.logger.trace("{}: Updating Meter (accumulated)", thingHandler.thingName);
189 double currentWatts = 0.0;
190 double totalWatts = 0.0;
191 double lastMin1 = 0.0;
193 String groupName = CHANNEL_GROUP_METER;
194 for (ShellySettingsMeter meter : status.meters) {
196 currentWatts += getDouble(meter.power);
197 totalWatts += getDouble(meter.total);
198 if (meter.counters != null) {
199 lastMin1 += getDouble(meter.counters[0]);
201 if (getLong(meter.timestamp) > timestamp) {
202 timestamp = getLong(meter.timestamp); // newest one
206 // Create channels for 1 Meter
207 if (!thingHandler.areChannelsCreated()) {
208 thingHandler.updateChannelDefinitions(ShellyChannelDefinitions
209 .createMeterChannels(thingHandler.getThing(), status.meters.get(0), groupName));
212 updated |= thingHandler.updateChannel(groupName, CHANNEL_METER_LASTMIN1,
213 toQuantityType(getDouble(lastMin1), DIGITS_WATT, Units.WATT));
215 // convert totalWatts into kw/h
216 totalWatts = totalWatts / (60.0 * 1000.0);
217 updated |= thingHandler.updateChannel(groupName, CHANNEL_METER_CURRENTWATTS,
218 toQuantityType(getDouble(currentWatts), DIGITS_WATT, Units.WATT));
219 updated |= thingHandler.updateChannel(groupName, CHANNEL_METER_TOTALKWH,
220 toQuantityType(getDouble(totalWatts), DIGITS_KWH, Units.KILOWATT_HOUR));
222 if (updated && timestamp > 0) {
223 thingHandler.updateChannel(groupName, CHANNEL_LAST_UPDATE,
224 getTimestamp(getString(profile.settings.timezone), timestamp));
228 if (!profile.isRoller && !profile.isRGBW2) {
229 thingHandler.updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_ACCUWATTS,
230 toQuantityType(accumulatedWatts, DIGITS_WATT, Units.WATT));
231 thingHandler.updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_ACCUTOTAL,
232 toQuantityType(accumulatedTotal, DIGITS_KWH, Units.KILOWATT_HOUR));
233 thingHandler.updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_ACCURETURNED,
234 toQuantityType(accumulatedReturned, DIGITS_KWH, Units.KILOWATT_HOUR));
241 private static Double computePF(ShellySettingsEMeter emeter) {
242 if (emeter.pf != null) { // EM3
243 return emeter.pf; // take device value
246 // EM: compute from provided values
247 if (Math.abs(emeter.power) + Math.abs(emeter.reactive) > 1.5) {
248 double pf = emeter.power / Math.sqrt(emeter.power * emeter.power + emeter.reactive * emeter.reactive);
255 * Update Sensor channel
257 * @param th Thing Handler instance
258 * @param profile ShellyDeviceProfile
259 * @param status Last ShellySettingsStatus
261 * @throws ShellyApiException
263 public static boolean updateSensors(ShellyBaseHandler thingHandler, ShellySettingsStatus status)
264 throws ShellyApiException {
265 ShellyDeviceProfile profile = thingHandler.getProfile();
267 boolean updated = false;
268 if (profile.isSensor || profile.hasBattery) {
269 ShellyStatusSensor sdata = thingHandler.api.getSensorStatus();
270 if (!thingHandler.areChannelsCreated()) {
271 thingHandler.logger.trace("{}: Create missing sensor channel(s)", thingHandler.thingName);
272 thingHandler.updateChannelDefinitions(
273 ShellyChannelDefinitions.createSensorChannels(thingHandler.getThing(), profile, sdata));
276 updated |= thingHandler.updateWakeupReason(sdata.actReasons);
278 if ((sdata.sensor != null) && sdata.sensor.isValid) {
279 // Shelly DW: “sensor”:{“state”:“open”, “is_valid”:true},
280 updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_STATE,
281 getString(sdata.sensor.state).equalsIgnoreCase(SHELLY_API_DWSTATE_OPEN) ? OpenClosedType.OPEN
282 : OpenClosedType.CLOSED);
283 String sensorError = sdata.sensorError;
284 boolean changed = thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_ERROR,
285 getStringType(sensorError));
286 if (!"0".equals(sensorError) && changed) {
287 thingHandler.postEvent(getString(sdata.sensorError), true);
291 if ((sdata.tmp != null) && getBool(sdata.tmp.isValid)) {
292 thingHandler.logger.trace("{}: Updating temperature", thingHandler.thingName);
293 Double temp = getString(sdata.tmp.units).toUpperCase().equals(SHELLY_TEMP_CELSIUS)
294 ? getDouble(sdata.tmp.tC)
295 : getDouble(sdata.tmp.tF);
296 if (getString(sdata.tmp.units).toUpperCase().equals(SHELLY_TEMP_FAHRENHEIT)) {
297 // convert Fahrenheit to Celsius
298 temp = ImperialUnits.FAHRENHEIT.getConverterTo(SIUnits.CELSIUS).convert(temp).doubleValue();
300 temp = convertToC(temp, getString(sdata.tmp.units));
301 updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_TEMP,
302 toQuantityType(temp.doubleValue(), DIGITS_TEMP, SIUnits.CELSIUS));
303 } else if (status.thermostats != null && status.thermostats.size() > 0) {
305 ShellyThermnostat t = status.thermostats.get(0);
306 ShellyThermnostat ps = profile.settings.thermostats.get(0);
307 int bminutes = getInteger(t.boostMinutes) > 0 ? getInteger(t.boostMinutes)
308 : getInteger(ps.boostMinutes);
309 updated |= thingHandler.updateChannel(CHANNEL_GROUP_CONTROL, CHANNEL_CONTROL_BCONTROL,
310 getOnOff(getInteger(t.boostMinutes) > 0));
311 updated |= thingHandler.updateChannel(CHANNEL_GROUP_CONTROL, CHANNEL_CONTROL_BTIMER,
312 toQuantityType((double) bminutes, DIGITS_NONE, Units.MINUTE));
313 updated |= thingHandler.updateChannel(CHANNEL_GROUP_CONTROL, CHANNEL_CONTROL_MODE,
314 getStringType(getBool(t.targetTemp.enabled) ? SHELLY_TRV_MODE_AUTO : SHELLY_TRV_MODE_MANUAL));
315 updated |= thingHandler.updateChannel(CHANNEL_GROUP_CONTROL, CHANNEL_CONTROL_PROFILE,
316 getDecimal(getBool(t.schedule) ? t.profile : 0));
317 updated |= thingHandler.updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_SCHEDULE,
318 getOnOff(t.schedule));
320 Double temp = convertToC(t.tmp.value, getString(t.tmp.units));
321 // Some devices report values = -999 or 99 during fw update
322 boolean valid = temp.intValue() > -50 && temp.intValue() < 90;
323 updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_TEMP,
324 toQuantityType(temp.doubleValue(), DIGITS_TEMP, SIUnits.CELSIUS));
325 temp = convertToC(t.targetTemp.value, getString(t.targetTemp.unit));
326 updated |= thingHandler.updateChannel(CHANNEL_GROUP_CONTROL, CHANNEL_CONTROL_SETTEMP,
327 toQuantityType(t.targetTemp.value, DIGITS_TEMP, SIUnits.CELSIUS));
330 updated |= thingHandler.updateChannel(CHANNEL_GROUP_CONTROL, CHANNEL_CONTROL_POSITION,
331 t.pos != -1 ? toQuantityType(t.pos, DIGITS_NONE, Units.PERCENT) : UnDefType.UNDEF);
332 updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_STATE,
333 getDouble(t.pos) > 0 ? OpenClosedType.OPEN : OpenClosedType.CLOSED);
336 if (sdata.hum != null) {
337 thingHandler.logger.trace("{}: Updating humidity", thingHandler.thingName);
338 updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_HUM,
339 toQuantityType(getDouble(sdata.hum.value), DIGITS_PERCENT, Units.PERCENT));
341 if ((sdata.lux != null) && getBool(sdata.lux.isValid)) {
342 // “lux”:{“value”:30, “illumination”: “dark”, “is_valid”:true},
343 thingHandler.logger.trace("{}: Updating lux", thingHandler.thingName);
344 updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_LUX,
345 toQuantityType(getDouble(sdata.lux.value), DIGITS_LUX, Units.LUX));
346 if (sdata.lux.illumination != null) {
347 updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_ILLUM,
348 getStringType(sdata.lux.illumination));
351 if (sdata.accel != null) {
352 updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_TILT,
353 toQuantityType(getDouble(sdata.accel.tilt.doubleValue()), DIGITS_NONE, Units.DEGREE_ANGLE));
355 if (sdata.flood != null) {
356 updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_FLOOD,
357 getOnOff(sdata.flood));
359 if (sdata.smoke != null) {
360 updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_SMOKE,
361 getOnOff(sdata.smoke));
363 if (sdata.gasSensor != null) {
364 updated |= thingHandler.updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_SELFTTEST,
365 getStringType(sdata.gasSensor.selfTestState));
366 updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_ALARM_STATE,
367 getStringType(sdata.gasSensor.alarmState));
368 updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_SSTATE,
369 getStringType(sdata.gasSensor.sensorState));
371 if ((sdata.concentration != null) && sdata.concentration.isValid) {
372 updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_PPM,
373 getDecimal(sdata.concentration.ppm));
375 if ((sdata.adcs != null) && (sdata.adcs.size() > 0)) {
376 ShellyADC adc = sdata.adcs.get(0);
377 updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_VOLTAGE,
378 toQuantityType(getDouble(adc.voltage), 2, Units.VOLT));
381 boolean charger = (getInteger(profile.settings.externalPower) == 1) || getBool(sdata.charger);
382 if ((profile.settings.externalPower != null) || (sdata.charger != null)) {
383 updated |= thingHandler.updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_CHARGER,
384 charger ? OnOffType.ON : OnOffType.OFF);
386 if (sdata.bat != null) { // no update for Sense
387 // Shelly HT has external_power under settings, Sense and Motion charger under status
388 if (!charger || !profile.isHT) {
389 updated |= thingHandler.updateChannel(CHANNEL_GROUP_BATTERY, CHANNEL_SENSOR_BAT_LEVEL,
390 toQuantityType(getDouble(sdata.bat.value), 0, Units.PERCENT));
392 updated |= thingHandler.updateChannel(CHANNEL_GROUP_BATTERY, CHANNEL_SENSOR_BAT_LEVEL,
395 boolean changed = thingHandler.updateChannel(CHANNEL_GROUP_BATTERY, CHANNEL_SENSOR_BAT_LOW,
396 getDouble(sdata.bat.value) < thingHandler.config.lowBattery ? OnOffType.ON : OnOffType.OFF);
398 if (changed && getDouble(sdata.bat.value) < thingHandler.config.lowBattery) {
399 thingHandler.postEvent(ALARM_TYPE_LOW_BATTERY, false);
403 if (sdata.motion != null) { // Shelly Sense
404 updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_MOTION,
405 getOnOff(sdata.motion));
407 if (sdata.sensor != null) { // Shelly Motion
408 updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_MOTION_ACT,
409 getOnOff(sdata.sensor.motionActive));
410 updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_MOTION,
411 getOnOff(sdata.sensor.motion));
412 long timestamp = getLong(sdata.sensor.motionTimestamp);
413 if (timestamp != 0) {
414 updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_MOTION_TS,
415 getTimestamp(getString(profile.settings.timezone), timestamp));
419 updated |= thingHandler.updateInputs(status);
422 thingHandler.updateChannel(profile.getControlGroup(0), CHANNEL_LAST_UPDATE, getTimestamp());
428 private static Double convertToC(@Nullable Double temp, String unit) {
432 if (SHELLY_TEMP_FAHRENHEIT.equalsIgnoreCase(unit)) {
433 // convert Fahrenheit to Celsius
434 return ImperialUnits.FAHRENHEIT.getConverterTo(SIUnits.CELSIUS).convert(temp).doubleValue();