2 * Copyright (c) 2010-2024 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.api1.Shelly1ApiJsonDTO.*;
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.ShellyDeviceProfile;
23 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO;
24 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyRollerStatus;
25 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsDimmer;
26 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsEMeter;
27 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsMeter;
28 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsRelay;
29 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsStatus;
30 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyShortLightStatus;
31 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyStatusSensor;
32 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyStatusSensor.ShellyADC;
33 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyStatusSensor.ShellyExtTemperature.ShellyShortTemp;
34 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyThermnostat;
35 import org.openhab.binding.shelly.internal.provider.ShellyChannelDefinitions;
36 import org.openhab.core.library.types.OnOffType;
37 import org.openhab.core.library.types.StringType;
38 import org.openhab.core.library.unit.ImperialUnits;
39 import org.openhab.core.library.unit.SIUnits;
40 import org.openhab.core.library.unit.Units;
41 import org.openhab.core.types.UnDefType;
43 import com.google.gson.Gson;
46 * The{@link ShellyComponents} implements updates for supplemental components
47 * Meter will be used by Relay + Light; Sensor is part of H&T, Flood, Door Window, Sense
49 * @author Markus Michels - Initial contribution
52 public class ShellyComponents {
55 * Update device status
57 * @param thingHandler Thing Handler instance
58 * @param status Status message
60 public static boolean updateDeviceStatus(ShellyThingInterface thingHandler, ShellySettingsStatus status) {
61 ShellyDeviceProfile profile = thingHandler.getProfile();
63 if (!profile.gateway.isEmpty()) {
64 thingHandler.updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_GATEWAY, getStringType(profile.gateway));
66 if (!thingHandler.areChannelsCreated()) {
67 thingHandler.updateChannelDefinitions(ShellyChannelDefinitions.createDeviceChannels(thingHandler.getThing(),
68 thingHandler.getProfile(), status));
71 if (getLong(status.uptime) > 10) {
72 thingHandler.updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_UPTIME,
73 toQuantityType((double) getLong(status.uptime), DIGITS_NONE, Units.SECOND));
76 Integer rssi = getInteger(status.wifiSta.rssi);
77 thingHandler.updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_RSSI, mapSignalStrength(rssi));
78 if (status.tmp != null && getBool(status.tmp.isValid) && !thingHandler.getProfile().isSensor
79 && status.tmp.tC != SHELLY_API_INVTEMP) {
80 thingHandler.updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_ITEMP,
81 toQuantityType(getDouble(status.tmp.tC), DIGITS_TEMP, SIUnits.CELSIUS));
82 } else if (status.temperature != null && status.temperature != SHELLY_API_INVTEMP) {
83 thingHandler.updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_ITEMP,
84 toQuantityType(getDouble(status.temperature), DIGITS_NONE, SIUnits.CELSIUS));
86 thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_SLEEPTIME,
87 toQuantityType(getInteger(status.sleepTime), Units.SECOND));
89 thingHandler.updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_UPDATE, getOnOff(status.hasUpdate));
91 if (profile.settings.calibrated != null) {
92 thingHandler.updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_CALIBRATED,
93 getOnOff(profile.settings.calibrated));
96 return false; // device status never triggers update
99 public static boolean updateRelay(ShellyBaseHandler thingHandler, ShellySettingsStatus status, int id) {
100 ShellyDeviceProfile profile = thingHandler.getProfile();
101 ShellySettingsRelay relay = status.relays.get(id);
102 ShellySettingsRelay rsettings;
103 if (profile.settings.relays != null) {
104 rsettings = profile.settings.relays.get(id);
106 throw new IllegalArgumentException("No relay settings");
109 boolean updated = false;
110 if (relay.isValid == null || relay.isValid) {
111 String groupName = profile.getControlGroup(id);
112 updated |= thingHandler.updateChannel(groupName, CHANNEL_OUTPUT_NAME, getStringType(rsettings.name));
114 if (getBool(relay.overpower)) {
115 thingHandler.postEvent(ALARM_TYPE_OVERPOWER, false);
118 updated |= thingHandler.updateChannel(groupName, CHANNEL_OUTPUT, getOnOff(relay.ison));
119 updated |= thingHandler.updateChannel(groupName, CHANNEL_TIMER_ACTIVE, getOnOff(relay.hasTimer));
120 if (status.extSwitch != null) {
121 if (status.extSwitch.input0 != null) {
122 updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_ESENSOR_INPUT1,
123 getOpenClosed(getInteger(status.extSwitch.input0.input) == 1));
127 // Update Auto-ON/OFF timer
128 updated |= thingHandler.updateChannel(groupName, CHANNEL_TIMER_AUTOON,
129 toQuantityType(getDouble(rsettings.autoOn), Units.SECOND));
130 updated |= thingHandler.updateChannel(groupName, CHANNEL_TIMER_AUTOOFF,
131 toQuantityType(getDouble(rsettings.autoOff), Units.SECOND));
136 public static boolean updateRoller(ShellyBaseHandler thingHandler, ShellyRollerStatus control, int id)
137 throws ShellyApiException {
138 ShellyDeviceProfile profile = thingHandler.getProfile();
139 boolean updated = false;
140 if (getBool(control.isValid)) {
141 String groupName = profile.getControlGroup(id);
142 if (control.name != null) {
143 updated |= thingHandler.updateChannel(groupName, CHANNEL_OUTPUT_NAME, getStringType(control.name));
146 String state = getString(control.state);
149 case SHELLY_ALWD_ROLLER_TURN_OPEN:
150 pos = SHELLY_MAX_ROLLER_POS;
152 case SHELLY_ALWD_ROLLER_TURN_CLOSE:
153 pos = SHELLY_MIN_ROLLER_POS;
155 case SHELLY_ALWD_ROLLER_TURN_STOP:
156 if (control.currentPos != null) {
157 // only valid in stop state
158 pos = Math.max(SHELLY_MIN_ROLLER_POS, Math.min(control.currentPos, SHELLY_MAX_ROLLER_POS));
163 thingHandler.logger.debug("{}: Update roller position to {}/{}, state={}", thingHandler.thingName, pos,
164 SHELLY_MAX_ROLLER_POS - pos, state);
165 updated |= thingHandler.updateChannel(groupName, CHANNEL_ROL_CONTROL_CONTROL,
166 toQuantityType((double) (SHELLY_MAX_ROLLER_POS - pos), Units.PERCENT));
167 updated |= thingHandler.updateChannel(groupName, CHANNEL_ROL_CONTROL_POS,
168 toQuantityType((double) pos, Units.PERCENT));
171 updated |= thingHandler.updateChannel(groupName, CHANNEL_ROL_CONTROL_STATE, new StringType(state));
172 updated |= thingHandler.updateChannel(groupName, CHANNEL_ROL_CONTROL_STOPR,
173 getStringType(control.stopReason));
174 updated |= thingHandler.updateChannel(groupName, CHANNEL_ROL_CONTROL_SAFETY,
175 getOnOff(control.safetySwitch));
181 * Update Meter channel
183 * @param thingHandler Thing Handler instance
184 * @param status Last ShellySettingsStatus
186 public static boolean updateMeters(ShellyThingInterface thingHandler, ShellySettingsStatus status) {
187 ShellyDeviceProfile profile = thingHandler.getProfile();
189 double accumulatedWatts = 0.0;
190 double accumulatedTotal = 0.0;
191 double accumulatedReturned = 0.0;
193 boolean updated = false;
194 // Devices without power meters get no updates
196 // Roler+RGBW2 have multiple meters -> aggregate consumption to the functional device
197 // Meter and EMeter have a different set of channels
198 if (status.meters != null || status.emeters != null) {
199 if (!profile.isRoller && !profile.isRGBW2) {
200 // In Relay mode we map eacher meter to the matching channel group
202 if (!profile.isEMeter) {
203 for (ShellySettingsMeter meter : status.meters) {
204 if (m >= profile.numMeters) {
205 // Shelly1: reports status.meters[0].is_valid = true, but even doesn't have a meter
206 meter.isValid = false;
208 if (getBool(meter.isValid) || profile.isLight) { // RGBW2-white doesn't report valid flag
209 // correctly in white mode
210 String groupName = profile.getMeterGroup(m);
211 if (!thingHandler.areChannelsCreated()) {
212 // skip for Shelly Bulb: JSON has a meter, but values don't get updated
213 if (!profile.isBulb) {
214 thingHandler.updateChannelDefinitions(ShellyChannelDefinitions
215 .createMeterChannels(thingHandler.getThing(), meter, groupName));
219 updated |= thingHandler.updateChannel(groupName, CHANNEL_METER_CURRENTWATTS,
220 toQuantityType(getDouble(meter.power), DIGITS_WATT, Units.WATT));
221 accumulatedWatts += getDouble(meter.power);
223 // convert Watt/Min to kw/h
224 if (meter.total != null) {
225 double kwh = getDouble(meter.total) / 1000 / 60;
226 updated |= thingHandler.updateChannel(groupName, CHANNEL_METER_TOTALKWH,
227 toQuantityType(kwh, DIGITS_KWH, Units.KILOWATT_HOUR));
228 accumulatedTotal += kwh;
230 if (meter.counters != null) {
231 updated |= thingHandler.updateChannel(groupName, CHANNEL_METER_LASTMIN1,
232 toQuantityType(getDouble(meter.counters[0]), DIGITS_WATT, Units.WATT));
234 if (meter.timestamp != null) {
235 thingHandler.updateChannel(groupName, CHANNEL_LAST_UPDATE,
236 getTimestamp(getString(profile.settings.timezone), meter.timestamp));
242 if (status.neutralCurrent != null) {
243 if (!thingHandler.areChannelsCreated()) {
244 thingHandler.updateChannelDefinitions(ShellyChannelDefinitions.createEMNCurrentChannels(
245 thingHandler.getThing(), profile.settings.neutralCurrent, status.neutralCurrent));
247 if (getBool(status.neutralCurrent.isValid)) {
248 String ngroup = CHANNEL_GROUP_NMETER;
249 updated |= thingHandler.updateChannel(ngroup, CHANNEL_NMETER_CURRENT, toQuantityType(
250 getDouble(status.neutralCurrent.current), DIGITS_AMPERE, Units.AMPERE));
251 updated |= thingHandler.updateChannel(ngroup, CHANNEL_NMETER_IXSUM, toQuantityType(
252 getDouble(status.neutralCurrent.ixsum), DIGITS_AMPERE, Units.AMPERE));
253 updated |= thingHandler.updateChannel(ngroup, CHANNEL_NMETER_MTRESHHOLD,
254 toQuantityType(getDouble(profile.settings.neutralCurrent.mismatchThreshold),
255 DIGITS_AMPERE, Units.AMPERE));
256 updated |= thingHandler.updateChannel(ngroup, CHANNEL_NMETER_MISMATCH,
257 getOnOff(status.neutralCurrent.mismatch));
261 for (ShellySettingsEMeter emeter : status.emeters) {
262 if (getBool(emeter.isValid)) {
263 String groupName = profile.getMeterGroup(m);
264 if (!thingHandler.areChannelsCreated()) {
265 thingHandler.updateChannelDefinitions(ShellyChannelDefinitions
266 .createEMeterChannels(thingHandler.getThing(), profile, emeter, groupName));
269 // convert Watt/h to KW/h
270 double total = getDouble(emeter.total) / 1000;
271 double totalReturned = getDouble(emeter.totalReturned) / 1000;
272 updated |= thingHandler.updateChannel(groupName, CHANNEL_METER_CURRENTWATTS,
273 toQuantityType(getDouble(emeter.power), DIGITS_WATT, Units.WATT));
274 updated |= thingHandler.updateChannel(groupName, CHANNEL_METER_TOTALKWH,
275 toQuantityType(total, DIGITS_KWH, Units.KILOWATT_HOUR));
276 updated |= thingHandler.updateChannel(groupName, CHANNEL_EMETER_TOTALRET,
277 toQuantityType(totalReturned, DIGITS_KWH, Units.KILOWATT_HOUR));
278 updated |= thingHandler.updateChannel(groupName, CHANNEL_EMETER_REACTWATTS,
279 toQuantityType(getDouble(emeter.reactive), DIGITS_WATT, Units.WATT));
280 updated |= thingHandler.updateChannel(groupName, CHANNEL_EMETER_VOLTAGE,
281 toQuantityType(getDouble(emeter.voltage), DIGITS_VOLT, Units.VOLT));
282 updated |= thingHandler.updateChannel(groupName, CHANNEL_EMETER_CURRENT,
283 toQuantityType(getDouble(emeter.current), DIGITS_AMPERE, Units.AMPERE));
284 updated |= thingHandler.updateChannel(groupName, CHANNEL_EMETER_PFACTOR,
285 toQuantityType(computePF(emeter), Units.PERCENT));
287 accumulatedWatts += getDouble(emeter.power);
288 accumulatedTotal += total;
289 accumulatedReturned += totalReturned;
291 thingHandler.updateChannel(groupName, CHANNEL_LAST_UPDATE, getTimestamp());
298 // In Roller Mode we accumulate all meters to a single set of meters
299 double currentWatts = 0.0;
300 double totalWatts = 0.0;
301 double lastMin1 = 0.0;
303 String groupName = CHANNEL_GROUP_METER;
305 if (!thingHandler.areChannelsCreated()) {
306 ShellySettingsMeter m = status.meters.get(0);
307 if (getBool(m.isValid)) {
308 // Create channels for 1 Meter
309 thingHandler.updateChannelDefinitions(
310 ShellyChannelDefinitions.createMeterChannels(thingHandler.getThing(), m, groupName));
314 for (ShellySettingsMeter meter : status.meters) {
315 if (getBool(meter.isValid)) {
316 currentWatts += getDouble(meter.power);
317 totalWatts += getDouble(meter.total);
318 if (meter.counters != null) {
319 lastMin1 += getDouble(meter.counters[0]);
321 if (getLong(meter.timestamp) > timestamp) {
322 timestamp = getLong(meter.timestamp); // newest one
327 updated |= thingHandler.updateChannel(groupName, CHANNEL_METER_LASTMIN1,
328 toQuantityType(getDouble(lastMin1), DIGITS_WATT, Units.WATT));
330 // convert totalWatts into kw/h
331 totalWatts = totalWatts / (60.0 * 1000.0);
332 updated |= thingHandler.updateChannel(groupName, CHANNEL_METER_CURRENTWATTS,
333 toQuantityType(currentWatts, DIGITS_WATT, Units.WATT));
334 updated |= thingHandler.updateChannel(groupName, CHANNEL_METER_TOTALKWH,
335 toQuantityType(totalWatts, DIGITS_KWH, Units.KILOWATT_HOUR));
337 if (updated && timestamp > 0) {
338 thingHandler.updateChannel(groupName, CHANNEL_LAST_UPDATE,
339 getTimestamp(getString(profile.settings.timezone), timestamp));
343 if (!profile.isRoller && !profile.isRGBW2) {
344 thingHandler.updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_ACCUWATTS, toQuantityType(
345 status.totalPower != null ? status.totalPower : accumulatedWatts, DIGITS_WATT, Units.WATT));
346 thingHandler.updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_ACCUTOTAL,
347 toQuantityType(status.totalCurrent != null ? status.totalCurrent / 1000 : accumulatedTotal,
348 DIGITS_KWH, Units.KILOWATT_HOUR));
349 thingHandler.updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_ACCURETURNED,
350 toQuantityType(status.totalReturned != null ? status.totalReturned / 1000 : accumulatedReturned,
351 DIGITS_KWH, Units.KILOWATT_HOUR));
358 private static Double computePF(ShellySettingsEMeter emeter) {
359 if (emeter.pf != null) { // EM3
360 return emeter.pf; // take device value
363 // EM: compute from provided values
364 if (emeter.reactive != null && Math.abs(emeter.power) + Math.abs(emeter.reactive) > 1.5) {
365 return emeter.power / Math.sqrt(emeter.power * emeter.power + emeter.reactive * emeter.reactive);
371 * Update Sensor channel
373 * @param thingHandler Thing Handler instance
374 * @param status Last ShellySettingsStatus
376 * @throws ShellyApiException
378 public static boolean updateSensors(ShellyThingInterface thingHandler, ShellySettingsStatus status)
379 throws ShellyApiException {
380 ShellyDeviceProfile profile = thingHandler.getProfile();
382 boolean updated = false;
383 if (profile.isSensor || profile.hasBattery) {
384 ShellyStatusSensor sdata = thingHandler.getApi().getSensorStatus();
385 if (!thingHandler.areChannelsCreated()) {
386 thingHandler.updateChannelDefinitions(
387 ShellyChannelDefinitions.createSensorChannels(thingHandler.getThing(), profile, sdata));
390 updated |= thingHandler.updateWakeupReason(sdata.actReasons);
392 if ((sdata.sensor != null) && sdata.sensor.isValid) {
393 // Shelly DW: “sensor”:{“state”:“open”, “is_valid”:true},
394 updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_STATE,
395 getOpenClosed(getString(sdata.sensor.state).equalsIgnoreCase(SHELLY_API_DWSTATE_OPEN)));
396 String sensorError = sdata.sensorError;
397 boolean changed = thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_ERROR,
398 getStringType(sensorError));
399 if (changed && !"0".equals(sensorError)) {
400 thingHandler.postEvent(getString(sdata.sensorError), true);
404 if (sdata.tmp != null && getBool(sdata.tmp.isValid)) {
405 Double temp = getString(sdata.tmp.units).toUpperCase().equals(SHELLY_TEMP_CELSIUS)
406 ? getDouble(sdata.tmp.tC)
407 : getDouble(sdata.tmp.tF);
408 updated |= updateTempChannel(thingHandler, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_TEMP,
409 temp.doubleValue(), getString(sdata.tmp.units));
410 } else if (status.thermostats != null) {
412 if (profile.settings.thermostats != null) {
413 ShellyThermnostat ps = profile.settings.thermostats.get(0);
414 ShellyThermnostat t = status.thermostats.get(0);
415 int bminutes = getInteger(t.boostMinutes) >= 0 ? getInteger(t.boostMinutes)
416 : getInteger(ps.boostMinutes);
417 updated |= thingHandler.updateChannel(CHANNEL_GROUP_CONTROL, CHANNEL_CONTROL_BCONTROL,
418 getOnOff(getInteger(t.boostMinutes) > 0));
419 updated |= thingHandler.updateChannel(CHANNEL_GROUP_CONTROL, CHANNEL_CONTROL_BTIMER,
420 toQuantityType((double) bminutes, DIGITS_NONE, Units.MINUTE));
421 updated |= thingHandler.updateChannel(CHANNEL_GROUP_CONTROL, CHANNEL_CONTROL_MODE, getStringType(
422 getBool(t.targetTemp.enabled) ? SHELLY_TRV_MODE_AUTO : SHELLY_TRV_MODE_MANUAL));
424 int pid = getBool(t.schedule) ? getInteger(t.profile) : 0;
425 updated |= thingHandler.updateChannel(CHANNEL_GROUP_CONTROL, CHANNEL_CONTROL_SCHEDULE,
426 getOnOff(t.schedule));
427 updated |= thingHandler.updateChannel(CHANNEL_GROUP_CONTROL, CHANNEL_CONTROL_PROFILE,
428 getStringType(profile.getValueProfile(0, pid)));
430 updated |= updateTempChannel(thingHandler, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_TEMP,
431 t.tmp.value, t.tmp.units);
432 if (t.targetTemp.unit == null) {
433 t.targetTemp.unit = t.tmp.units;
435 updated |= updateTempChannel(thingHandler, CHANNEL_GROUP_CONTROL, CHANNEL_CONTROL_SETTEMP,
436 t.targetTemp.value, t.targetTemp.unit);
439 updated |= thingHandler.updateChannel(CHANNEL_GROUP_CONTROL, CHANNEL_CONTROL_POSITION,
440 t.pos != -1 ? toQuantityType(t.pos, DIGITS_NONE, Units.PERCENT) : UnDefType.UNDEF);
441 updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_STATE,
442 getOpenClosed(getDouble(t.pos) > 0));
447 if (sdata.hum != null) {
448 updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_HUM,
449 toQuantityType(getDouble(sdata.hum.value), DIGITS_PERCENT, Units.PERCENT));
451 if ((sdata.lux != null) && getBool(sdata.lux.isValid)) {
452 // “lux”:{“value”:30, “illumination”: “dark”, “is_valid”:true},
453 updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_LUX,
454 toQuantityType(getDouble(sdata.lux.value), DIGITS_LUX, Units.LUX));
455 if (sdata.lux.illumination != null) {
456 updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_ILLUM,
457 getStringType(sdata.lux.illumination));
460 if (sdata.accel != null) {
461 updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_TILT,
462 toQuantityType(getDouble(sdata.accel.tilt.doubleValue()), DIGITS_NONE, Units.DEGREE_ANGLE));
464 if (sdata.flood != null) {
465 updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_FLOOD,
466 getOnOff(sdata.flood));
468 if (sdata.smoke != null) {
469 updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_SMOKE,
470 getOnOff(sdata.smoke));
472 if (sdata.mute != null) {
473 updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_MUTE, getOnOff(sdata.mute));
476 if (sdata.gasSensor != null) {
477 updated |= thingHandler.updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_SELFTTEST,
478 getStringType(sdata.gasSensor.selfTestState));
479 updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_ALARM_STATE,
480 getStringType(sdata.gasSensor.alarmState));
481 updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_SSTATE,
482 getStringType(sdata.gasSensor.sensorState));
484 if ((sdata.concentration != null) && sdata.concentration.isValid) {
485 updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_PPM, toQuantityType(
486 getInteger(sdata.concentration.ppm).doubleValue(), DIGITS_NONE, Units.PARTS_PER_MILLION));
488 if ((sdata.adcs != null) && (!sdata.adcs.isEmpty())) {
489 ShellyADC adc = sdata.adcs.get(0);
490 updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_VOLTAGE,
491 toQuantityType(getDouble(adc.voltage), 2, Units.VOLT));
494 boolean charger = (getInteger(profile.settings.externalPower) == 1) || getBool(sdata.charger);
495 if ((profile.settings.externalPower != null) || (sdata.charger != null)) {
496 updated |= thingHandler.updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_CHARGER,
499 if (sdata.bat != null) { // no update for Sense
500 updated |= thingHandler.updateChannel(CHANNEL_GROUP_BATTERY, CHANNEL_SENSOR_BAT_LEVEL,
501 toQuantityType(getDouble(sdata.bat.value), 0, Units.PERCENT));
503 int lowBattery = thingHandler.getThingConfig().lowBattery;
504 boolean changed = thingHandler.updateChannel(CHANNEL_GROUP_BATTERY, CHANNEL_SENSOR_BAT_LOW,
505 getOnOff(!charger && getDouble(sdata.bat.value) < lowBattery));
507 if (!charger && changed && getDouble(sdata.bat.value) < lowBattery) {
508 thingHandler.postEvent(ALARM_TYPE_LOW_BATTERY, false);
512 if (sdata.motion != null) { // Shelly Sense
513 updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_MOTION,
514 getOnOff(sdata.motion));
516 if (sdata.sensor != null) { // Shelly Motion
517 updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_MOTION_ACT,
518 getOnOff(sdata.sensor.motionActive));
519 updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_MOTION,
520 getOnOff(sdata.sensor.motion));
521 long timestamp = getLong(sdata.sensor.motionTimestamp);
522 if (timestamp != 0) {
523 updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_MOTION_TS,
524 getTimestamp(getString(profile.settings.timezone), timestamp));
528 updated |= thingHandler.updateInputs(status);
531 thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_LAST_UPDATE, getTimestamp());
535 // Update Add-On channeös
536 if (status.extTemperature != null) {
537 // Shelly 1/1PM support up to 3 external sensors
538 // for whatever reason those are not represented as an array, but 3 elements
539 updated |= updateTempChannel(status.extTemperature.sensor1, thingHandler, CHANNEL_ESENSOR_TEMP1);
540 updated |= updateTempChannel(status.extTemperature.sensor2, thingHandler, CHANNEL_ESENSOR_TEMP2);
541 updated |= updateTempChannel(status.extTemperature.sensor3, thingHandler, CHANNEL_ESENSOR_TEMP3);
542 updated |= updateTempChannel(status.extTemperature.sensor4, thingHandler, CHANNEL_ESENSOR_TEMP4);
543 updated |= updateTempChannel(status.extTemperature.sensor5, thingHandler, CHANNEL_ESENSOR_TEMP5);
545 if ((status.extHumidity != null) && (status.extHumidity.sensor1 != null)) {
546 updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_ESENSOR_HUMIDITY,
547 toQuantityType(getDouble(status.extHumidity.sensor1.hum), DIGITS_PERCENT, Units.PERCENT));
549 if ((status.extVoltage != null) && (status.extVoltage.sensor1 != null)) {
550 updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_ESENSOR_VOLTAGE,
551 toQuantityType(getDouble(status.extVoltage.sensor1.voltage), 4, Units.VOLT));
553 if ((status.extDigitalInput != null) && (status.extDigitalInput.sensor1 != null)) {
554 updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_ESENSOR_DIGITALINPUT,
555 getOnOff(status.extDigitalInput.sensor1.state));
557 if ((status.extAnalogInput != null) && (status.extAnalogInput.sensor1 != null)) {
558 updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_ESENSOR_ANALOGINPUT,
559 toQuantityType(getDouble(status.extAnalogInput.sensor1.percent), DIGITS_PERCENT, Units.PERCENT));
565 public static boolean updateDimmers(ShellyThingInterface thingHandler, ShellySettingsStatus orgStatus)
566 throws ShellyApiException {
567 boolean updated = false;
568 ShellyDeviceProfile profile = thingHandler.getProfile();
569 if (profile.isDimmer) {
570 // We need to fixup the returned Json: The dimmer returns light[] element, which is ok, but it doesn't have
571 // the same structure as lights[] from Bulb,RGBW2 and Duo. The tag gets replaced by dimmers[] so that Gson
572 // maps to a different structure (ShellyShortLight).
573 Gson gson = new Gson();
574 ShellySettingsStatus dstatus = !profile.isGen2
575 ? fromJson(gson, Shelly1ApiJsonDTO.fixDimmerJson(orgStatus.json), ShellySettingsStatus.class)
579 for (ShellyShortLightStatus dimmer : dstatus.dimmers) {
581 String groupName = profile.numRelays <= 1 ? CHANNEL_GROUP_DIMMER_CONTROL
582 : CHANNEL_GROUP_DIMMER_CONTROL + r.toString();
584 if (!thingHandler.areChannelsCreated()) {
585 thingHandler.updateChannelDefinitions(ShellyChannelDefinitions
586 .createDimmerChannels(thingHandler.getThing(), profile, dstatus, l));
589 ShellySettingsDimmer ds = profile.settings.dimmers.get(l);
590 if (ds.name != null) {
591 updated |= thingHandler.updateChannel(groupName, CHANNEL_OUTPUT_NAME, getStringType(ds.name));
594 // On a status update we map a dimmer.ison = false to brightness 0 rather than the device's brightness
595 // and send an OFF status to the same channel.
596 // When the device's brightness is > 0 we send the new value to the channel and an ON command
597 if (dimmer.ison != null) {
599 updated |= thingHandler.updateChannel(groupName, CHANNEL_BRIGHTNESS + "$Switch", OnOffType.ON);
600 updated |= thingHandler.updateChannel(groupName, CHANNEL_BRIGHTNESS + "$Value",
601 toQuantityType((double) getInteger(dimmer.brightness), DIGITS_NONE, Units.PERCENT));
603 updated |= thingHandler.updateChannel(groupName, CHANNEL_BRIGHTNESS + "$Switch", OnOffType.OFF);
604 updated |= thingHandler.updateChannel(groupName, CHANNEL_BRIGHTNESS + "$Value",
605 toQuantityType(0.0, DIGITS_NONE, Units.PERCENT));
609 if (profile.settings.dimmers != null) {
610 ShellySettingsDimmer dsettings = profile.settings.dimmers.get(l);
611 if (dsettings != null) {
612 updated |= thingHandler.updateChannel(groupName, CHANNEL_TIMER_AUTOON,
613 toQuantityType(getDouble(dsettings.autoOn), Units.SECOND));
614 updated |= thingHandler.updateChannel(groupName, CHANNEL_TIMER_AUTOOFF,
615 toQuantityType(getDouble(dsettings.autoOff), Units.SECOND));
625 public static boolean updateTempChannel(@Nullable ShellyShortTemp sensor, ShellyThingInterface thingHandler,
627 return sensor != null ? updateTempChannel(thingHandler, CHANNEL_GROUP_SENSOR, channel, sensor.tC, "") : false;
630 public static boolean updateTempChannel(ShellyThingInterface thingHandler, String group, String channel,
631 @Nullable Double temp, @Nullable String unit) {
632 if (temp == null || temp == SHELLY_API_INVTEMP) {
635 return thingHandler.updateChannel(group, channel,
636 toQuantityType(convertToC(temp, unit), DIGITS_TEMP, SIUnits.CELSIUS));
639 private static Double convertToC(@Nullable Double temp, @Nullable String unit) {
640 if (temp == null || temp == SHELLY_API_INVTEMP) {
641 return SHELLY_API_INVTEMP;
643 if (SHELLY_TEMP_FAHRENHEIT.equalsIgnoreCase(getString(unit))) {
644 // convert Fahrenheit to Celsius
645 return ImperialUnits.FAHRENHEIT.getConverterTo(SIUnits.CELSIUS).convert(temp).doubleValue();