2 * Copyright (c) 2010-2020 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 java.io.IOException;
21 import org.eclipse.jdt.annotation.NonNullByDefault;
22 import org.openhab.binding.shelly.internal.api.ShellyApiException;
23 import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsEMeter;
24 import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsMeter;
25 import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsStatus;
26 import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellyStatusSensor;
27 import org.openhab.binding.shelly.internal.api.ShellyDeviceProfile;
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.SmartHomeUnits;
34 import tec.uom.se.unit.Units;
37 * The{@link ShellyComponents} implements updates for supplemental components
38 * Meter will be used by Relay + Light; Sensor is part of H&T, Flood, Door Window, Sense
40 * @author Markus Michels - Initial contribution
43 public class ShellyComponents {
46 * Update device status
48 * @param th Thing Handler instance
49 * @param profile ShellyDeviceProfile
51 public static boolean updateDeviceStatus(ShellyBaseHandler thingHandler, ShellySettingsStatus status) {
52 if (!thingHandler.areChannelsCreated()) {
53 thingHandler.updateChannelDefinitions(ShellyChannelDefinitionsDTO
54 .createDeviceChannels(thingHandler.getThing(), thingHandler.getProfile(), status));
57 Integer rssi = getInteger(status.wifiSta.rssi);
58 thingHandler.updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_UPTIME,
59 toQuantityType(new Double(getLong(status.uptime)), DIGITS_NONE, SmartHomeUnits.SECOND));
60 thingHandler.updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_RSSI, mapSignalStrength(rssi));
61 if (status.tmp != null) {
62 thingHandler.updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_ITEMP,
63 toQuantityType(getDouble(status.tmp.tC), DIGITS_NONE, SIUnits.CELSIUS));
64 } else if (status.temperature != null) {
65 thingHandler.updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_ITEMP,
66 toQuantityType(getDouble(status.temperature), DIGITS_NONE, SIUnits.CELSIUS));
68 thingHandler.updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_UPDATE, getOnOff(status.hasUpdate));
70 return false; // device status never triggers update
74 * Update Meter channel
76 * @param th Thing Handler instance
77 * @param profile ShellyDeviceProfile
78 * @param status Last ShellySettingsStatus
80 public static boolean updateMeters(ShellyBaseHandler thingHandler, ShellySettingsStatus status) {
81 ShellyDeviceProfile profile = thingHandler.getProfile();
83 double accumulatedWatts = 0.0;
84 double accumulatedTotal = 0.0;
85 double accumulatedReturned = 0.0;
87 boolean updated = false;
88 // Devices without power meters get no updates
90 // Roler+RGBW2 have multiple meters -> aggregate consumption to the functional device
91 // Meter and EMeter have a different set of channels
92 if ((profile.numMeters > 0) && ((status.meters != null) || (status.emeters != null))) {
93 if (!profile.isRoller && !profile.isRGBW2) {
94 thingHandler.logger.trace("{}: Updating {} {}meter(s)", thingHandler.thingName, profile.numMeters,
95 !profile.isEMeter ? "standard " : "e-");
97 // In Relay mode we map eacher meter to the matching channel group
99 if (!profile.isEMeter) {
100 for (ShellySettingsMeter meter : status.meters) {
101 Integer meterIndex = m + 1;
102 if (getBool(meter.isValid) || profile.isLight) { // RGBW2-white doesn't report valid flag
103 // correctly in white mode
104 String groupName = "";
105 if (profile.numMeters > 1) {
106 groupName = CHANNEL_GROUP_METER + meterIndex.toString();
108 groupName = CHANNEL_GROUP_METER;
111 if (!thingHandler.areChannelsCreated()) {
112 // skip for Shelly Bulb: JSON has a meter, but values don't get updated
113 if (!profile.isBulb) {
114 thingHandler.updateChannelDefinitions(ShellyChannelDefinitionsDTO
115 .createMeterChannels(thingHandler.getThing(), meter, groupName));
119 updated |= thingHandler.updateChannel(groupName, CHANNEL_METER_CURRENTWATTS,
120 toQuantityType(getDouble(meter.power), DIGITS_WATT, SmartHomeUnits.WATT));
121 accumulatedWatts += getDouble(meter.power);
123 // convert Watt/Min to kw/h
124 if (meter.total != null) {
125 double kwh = getDouble(meter.total) / 60 / 1000;
126 updated |= thingHandler.updateChannel(groupName, CHANNEL_METER_TOTALKWH,
127 toQuantityType(kwh, DIGITS_KWH, SmartHomeUnits.KILOWATT_HOUR));
128 accumulatedTotal += kwh;
130 if (meter.counters != null) {
131 updated |= thingHandler.updateChannel(groupName, CHANNEL_METER_LASTMIN1,
132 toQuantityType(getDouble(meter.counters[0]), DIGITS_WATT, SmartHomeUnits.WATT));
134 thingHandler.updateChannel(groupName, CHANNEL_LAST_UPDATE,
135 getTimestamp(getString(profile.settings.timezone), getLong(meter.timestamp)));
140 for (ShellySettingsEMeter emeter : status.emeters) {
141 Integer meterIndex = m + 1;
142 if (getBool(emeter.isValid)) {
143 String groupName = profile.numMeters > 1 ? CHANNEL_GROUP_METER + meterIndex.toString()
144 : CHANNEL_GROUP_METER;
145 if (!thingHandler.areChannelsCreated()) {
146 thingHandler.updateChannelDefinitions(ShellyChannelDefinitionsDTO
147 .createEMeterChannels(thingHandler.getThing(), emeter, groupName));
150 // convert Watt/Hour tok w/h
151 updated |= thingHandler.updateChannel(groupName, CHANNEL_METER_CURRENTWATTS,
152 toQuantityType(getDouble(emeter.power), DIGITS_WATT, SmartHomeUnits.WATT));
153 updated |= thingHandler.updateChannel(groupName, CHANNEL_METER_TOTALKWH, toQuantityType(
154 getDouble(emeter.total) / 1000, DIGITS_KWH, SmartHomeUnits.KILOWATT_HOUR));
155 updated |= thingHandler.updateChannel(groupName, CHANNEL_EMETER_TOTALRET, toQuantityType(
156 getDouble(emeter.totalReturned) / 1000, DIGITS_KWH, SmartHomeUnits.KILOWATT_HOUR));
157 updated |= thingHandler.updateChannel(groupName, CHANNEL_EMETER_REACTWATTS,
158 toQuantityType(getDouble(emeter.reactive), DIGITS_WATT, SmartHomeUnits.WATT));
159 updated |= thingHandler.updateChannel(groupName, CHANNEL_EMETER_VOLTAGE,
160 toQuantityType(getDouble(emeter.voltage), DIGITS_VOLT, SmartHomeUnits.VOLT));
162 if (emeter.current != null) {
164 updated |= thingHandler.updateChannel(groupName, CHANNEL_EMETER_CURRENT,
165 toQuantityType(getDouble(emeter.current), DIGITS_VOLT, SmartHomeUnits.AMPERE));
166 updated |= thingHandler.updateChannel(groupName, CHANNEL_EMETER_PFACTOR,
167 getDecimal(emeter.pf));
170 accumulatedWatts += getDouble(emeter.power);
171 accumulatedTotal += getDouble(emeter.total) / 1000;
172 accumulatedReturned += getDouble(emeter.totalReturned) / 1000;
174 thingHandler.updateChannel(groupName, CHANNEL_LAST_UPDATE, getTimestamp());
181 // In Roller Mode we accumulate all meters to a single set of meters
182 thingHandler.logger.trace("{}: Updating Meter (accumulated)", thingHandler.thingName);
183 double currentWatts = 0.0;
184 double totalWatts = 0.0;
185 double lastMin1 = 0.0;
187 String groupName = CHANNEL_GROUP_METER;
188 for (ShellySettingsMeter meter : status.meters) {
190 currentWatts += getDouble(meter.power);
191 totalWatts += getDouble(meter.total);
192 if (meter.counters != null) {
193 lastMin1 += getDouble(meter.counters[0]);
195 if (getLong(meter.timestamp) > timestamp) {
196 timestamp = getLong(meter.timestamp); // newest one
200 // Create channels for 1 Meter
201 if (!thingHandler.areChannelsCreated()) {
202 thingHandler.updateChannelDefinitions(ShellyChannelDefinitionsDTO
203 .createMeterChannels(thingHandler.getThing(), status.meters.get(0), groupName));
206 updated |= thingHandler.updateChannel(groupName, CHANNEL_METER_LASTMIN1,
207 toQuantityType(getDouble(lastMin1), DIGITS_WATT, SmartHomeUnits.WATT));
209 // convert totalWatts into kw/h
210 totalWatts = totalWatts / (60.0 * 10000.0);
211 updated |= thingHandler.updateChannel(groupName, CHANNEL_METER_CURRENTWATTS,
212 toQuantityType(getDouble(currentWatts), DIGITS_WATT, SmartHomeUnits.WATT));
213 updated |= thingHandler.updateChannel(groupName, CHANNEL_METER_TOTALKWH,
214 toQuantityType(getDouble(totalWatts), DIGITS_KWH, SmartHomeUnits.KILOWATT_HOUR));
217 thingHandler.updateChannel(groupName, CHANNEL_LAST_UPDATE,
218 getTimestamp(getString(profile.settings.timezone), timestamp));
222 if (updated && !profile.isRoller && !profile.isRGBW2) {
223 thingHandler.updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_ACCUWATTS,
224 toQuantityType(accumulatedWatts, DIGITS_WATT, SmartHomeUnits.WATT));
225 thingHandler.updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_ACCUTOTAL,
226 toQuantityType(accumulatedTotal, DIGITS_KWH, SmartHomeUnits.KILOWATT_HOUR));
227 thingHandler.updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_ACCURETURNED,
228 toQuantityType(accumulatedReturned, DIGITS_KWH, SmartHomeUnits.KILOWATT_HOUR));
236 * Update Sensor channel
238 * @param th Thing Handler instance
239 * @param profile ShellyDeviceProfile
240 * @param status Last ShellySettingsStatus
242 * @throws IOException
244 public static boolean updateSensors(ShellyBaseHandler thingHandler, ShellySettingsStatus status)
245 throws ShellyApiException {
246 ShellyDeviceProfile profile = thingHandler.getProfile();
248 boolean updated = false;
249 if (profile.isSensor || profile.hasBattery || profile.isSense) {
250 ShellyStatusSensor sdata = thingHandler.api.getSensorStatus();
252 if (!thingHandler.areChannelsCreated()) {
253 thingHandler.logger.trace("{}: Create missing sensor channel(s)", thingHandler.thingName);
254 thingHandler.updateChannelDefinitions(
255 ShellyChannelDefinitionsDTO.createSensorChannels(thingHandler.getThing(), profile, sdata));
258 updated |= thingHandler.updateWakeupReason(sdata.actReasons);
260 if ((sdata.contact != null) && sdata.contact.isValid) {
261 // Shelly DW: “sensor”:{“state”:“open”, “is_valid”:true},
262 updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_CONTACT,
263 getString(sdata.contact.state).equalsIgnoreCase(SHELLY_API_DWSTATE_OPEN) ? OpenClosedType.OPEN
264 : OpenClosedType.CLOSED);
265 boolean changed = thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_ERROR,
266 getStringType(sdata.sensorError));
268 thingHandler.postEvent(sdata.sensorError, true);
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(Units.CELSIUS).convert(temp).doubleValue();
281 updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_TEMP,
282 toQuantityType(temp.doubleValue(), DIGITS_TEMP, SIUnits.CELSIUS));
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, SmartHomeUnits.PERCENT));
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, SmartHomeUnits.LUX));
294 if (sdata.lux.illumination != null) {
295 updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_ILLUM,
296 getStringType(sdata.lux.illumination));
299 if (sdata.accel != null) {
300 updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_TILT, toQuantityType(
301 getDouble(sdata.accel.tilt.doubleValue()), DIGITS_NONE, SmartHomeUnits.DEGREE_ANGLE));
302 updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_VIBRATION,
303 getInteger(sdata.accel.vibration) == 1 ? OnOffType.ON : OnOffType.OFF);
305 if (sdata.flood != null) {
306 updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_FLOOD,
307 getOnOff(sdata.flood));
309 if (sdata.smoke != null) {
310 updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_SMOKE,
311 getOnOff(sdata.smoke));
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));
321 if ((sdata.concentration != null) && sdata.concentration.isValid) {
322 updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_PPM,
323 getDecimal(sdata.concentration.ppm));
326 if (sdata.bat != null) { // no update for Sense
327 thingHandler.logger.trace("{}: Updating battery", thingHandler.thingName);
328 updated |= thingHandler.updateChannel(CHANNEL_GROUP_BATTERY, CHANNEL_SENSOR_BAT_LEVEL,
329 toQuantityType(getDouble(sdata.bat.value), DIGITS_PERCENT, SmartHomeUnits.PERCENT));
330 boolean changed = thingHandler.updateChannel(CHANNEL_GROUP_BATTERY, CHANNEL_SENSOR_BAT_LOW,
331 getDouble(sdata.bat.value) < thingHandler.config.lowBattery ? OnOffType.ON : OnOffType.OFF);
333 if (changed && getDouble(sdata.bat.value) < thingHandler.config.lowBattery) {
334 thingHandler.postEvent(ALARM_TYPE_LOW_BATTERY, false);
337 if (sdata.motion != null) {
338 updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_MOTION,
339 getOnOff(sdata.motion));
341 if (sdata.charger != null) {
342 updated |= thingHandler.updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_CHARGER,
343 getOnOff(sdata.charger));
346 updated |= thingHandler.updateInputs(status);
349 thingHandler.updateChannel(profile.getControlGroup(0), CHANNEL_LAST_UPDATE, getTimestamp());