]> git.basschouten.com Git - openhab-addons.git/blob
bdea3aa76ccddeb00bf2b0854151797c507d43d6
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 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.api1.Shelly1ApiJsonDTO.*;
17 import static org.openhab.binding.shelly.internal.util.ShellyUtils.*;
18
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;
42
43 import com.google.gson.Gson;
44
45 /***
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
48  *
49  * @author Markus Michels - Initial contribution
50  */
51 @NonNullByDefault
52 public class ShellyComponents {
53
54     /**
55      * Update device status
56      *
57      * @param th Thing Handler instance
58      * @param profile ShellyDeviceProfile
59      */
60     public static boolean updateDeviceStatus(ShellyThingInterface thingHandler, ShellySettingsStatus status) {
61         ShellyDeviceProfile profile = thingHandler.getProfile();
62
63         if (!profile.gateway.isEmpty()) {
64             thingHandler.updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_GATEWAY, getStringType(profile.gateway));
65         }
66         if (!thingHandler.areChannelsCreated()) {
67             thingHandler.updateChannelDefinitions(ShellyChannelDefinitions.createDeviceChannels(thingHandler.getThing(),
68                     thingHandler.getProfile(), status));
69         }
70
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));
74         }
75
76         Integer rssi = getInteger(status.wifiSta.rssi);
77         thingHandler.updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_RSSI, mapSignalStrength(rssi));
78         if (status.tmp != null && !thingHandler.getProfile().isSensor && status.tmp.tC != SHELLY_API_INVTEMP) {
79             thingHandler.updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_ITEMP,
80                     toQuantityType(getDouble(status.tmp.tC), DIGITS_NONE, SIUnits.CELSIUS));
81         } else if (status.temperature != null && status.temperature != SHELLY_API_INVTEMP) {
82             thingHandler.updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_ITEMP,
83                     toQuantityType(getDouble(status.temperature), DIGITS_NONE, SIUnits.CELSIUS));
84         }
85         thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_SLEEPTIME,
86                 toQuantityType(getInteger(status.sleepTime), Units.SECOND));
87
88         thingHandler.updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_UPDATE, getOnOff(status.hasUpdate));
89
90         if (profile.settings.calibrated != null) {
91             thingHandler.updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_CALIBRATED,
92                     getOnOff(profile.settings.calibrated));
93         }
94
95         return false; // device status never triggers update
96     }
97
98     public static boolean updateRelay(ShellyBaseHandler thingHandler, ShellySettingsStatus status, int id) {
99         ShellyDeviceProfile profile = thingHandler.getProfile();
100         ShellySettingsRelay relay = status.relays.get(id);
101         ShellySettingsRelay rsettings;
102         if (profile.settings.relays != null) {
103             rsettings = profile.settings.relays.get(id);
104         } else {
105             throw new IllegalArgumentException("No relay settings");
106         }
107
108         boolean updated = false;
109         if (relay.isValid == null || relay.isValid) {
110             String groupName = profile.getControlGroup(id);
111             updated |= thingHandler.updateChannel(groupName, CHANNEL_OUTPUT_NAME, getStringType(rsettings.name));
112
113             if (getBool(relay.overpower)) {
114                 thingHandler.postEvent(ALARM_TYPE_OVERPOWER, false);
115             }
116
117             updated |= thingHandler.updateChannel(groupName, CHANNEL_OUTPUT, getOnOff(relay.ison));
118             updated |= thingHandler.updateChannel(groupName, CHANNEL_TIMER_ACTIVE, getOnOff(relay.hasTimer));
119             if (status.extSwitch != null) {
120                 if (status.extSwitch.input0 != null) {
121                     updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_ESENSOR_INPUT1,
122                             getOpenClosed(getInteger(status.extSwitch.input0.input) == 1));
123                 }
124             }
125             if (status.extTemperature != null) {
126                 // Shelly 1/1PM support up to 3 external sensors
127                 // for whatever reason those are not represented as an array, but 3 elements
128                 updated |= updateTempChannel(status.extTemperature.sensor1, thingHandler, CHANNEL_ESENSOR_TEMP1);
129                 updated |= updateTempChannel(status.extTemperature.sensor2, thingHandler, CHANNEL_ESENSOR_TEMP2);
130                 updated |= updateTempChannel(status.extTemperature.sensor3, thingHandler, CHANNEL_ESENSOR_TEMP3);
131                 updated |= updateTempChannel(status.extTemperature.sensor4, thingHandler, CHANNEL_ESENSOR_TEMP4);
132                 updated |= updateTempChannel(status.extTemperature.sensor5, thingHandler, CHANNEL_ESENSOR_TEMP5);
133             }
134             if ((status.extHumidity != null) && (status.extHumidity.sensor1 != null)) {
135                 updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_ESENSOR_HUMIDITY,
136                         toQuantityType(getDouble(status.extHumidity.sensor1.hum), DIGITS_PERCENT, Units.PERCENT));
137             }
138             if ((status.extVoltage != null) && (status.extVoltage.sensor1 != null)) {
139                 updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_ESENSOR_VOLTAGE,
140                         toQuantityType(getDouble(status.extVoltage.sensor1.voltage), 4, Units.VOLT));
141             }
142             if ((status.extDigitalInput != null) && (status.extDigitalInput.sensor1 != null)) {
143                 updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_ESENSOR_DIGITALINPUT,
144                         getOnOff(status.extDigitalInput.sensor1.state));
145             }
146             if ((status.extAnalogInput != null) && (status.extAnalogInput.sensor1 != null)) {
147                 updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_ESENSOR_ANALOGINPUT, toQuantityType(
148                         getDouble(status.extAnalogInput.sensor1.percent), DIGITS_PERCENT, Units.PERCENT));
149             }
150
151             // Update Auto-ON/OFF timer
152             updated |= thingHandler.updateChannel(groupName, CHANNEL_TIMER_AUTOON,
153                     toQuantityType(getDouble(rsettings.autoOn), Units.SECOND));
154             updated |= thingHandler.updateChannel(groupName, CHANNEL_TIMER_AUTOOFF,
155                     toQuantityType(getDouble(rsettings.autoOff), Units.SECOND));
156         }
157         return updated;
158     }
159
160     public static boolean updateRoller(ShellyBaseHandler thingHandler, ShellyRollerStatus control, int id)
161             throws ShellyApiException {
162         ShellyDeviceProfile profile = thingHandler.getProfile();
163         boolean updated = false;
164         if (getBool(control.isValid)) {
165             String groupName = profile.getControlGroup(id);
166             if (control.name != null) {
167                 updated |= thingHandler.updateChannel(groupName, CHANNEL_OUTPUT_NAME, getStringType(control.name));
168             }
169
170             String state = getString(control.state);
171             int pos = -1;
172             switch (state) {
173                 case SHELLY_ALWD_ROLLER_TURN_OPEN:
174                     pos = SHELLY_MAX_ROLLER_POS;
175                     break;
176                 case SHELLY_ALWD_ROLLER_TURN_CLOSE:
177                     pos = SHELLY_MIN_ROLLER_POS;
178                     break;
179                 case SHELLY_ALWD_ROLLER_TURN_STOP:
180                     if (control.currentPos != null) {
181                         // only valid in stop state
182                         pos = Math.max(SHELLY_MIN_ROLLER_POS, Math.min(control.currentPos, SHELLY_MAX_ROLLER_POS));
183                     }
184                     break;
185             }
186             if (pos != -1) {
187                 thingHandler.logger.debug("{}: Update roller position to {}/{}, state={}", thingHandler.thingName, pos,
188                         SHELLY_MAX_ROLLER_POS - pos, state);
189                 updated |= thingHandler.updateChannel(groupName, CHANNEL_ROL_CONTROL_CONTROL,
190                         toQuantityType((double) (SHELLY_MAX_ROLLER_POS - pos), Units.PERCENT));
191                 updated |= thingHandler.updateChannel(groupName, CHANNEL_ROL_CONTROL_POS,
192                         toQuantityType((double) pos, Units.PERCENT));
193             }
194
195             updated |= thingHandler.updateChannel(groupName, CHANNEL_ROL_CONTROL_STATE, new StringType(state));
196             updated |= thingHandler.updateChannel(groupName, CHANNEL_ROL_CONTROL_STOPR,
197                     getStringType(control.stopReason));
198             updated |= thingHandler.updateChannel(groupName, CHANNEL_ROL_CONTROL_SAFETY,
199                     getOnOff(control.safetySwitch));
200         }
201         return updated;
202     }
203
204     /**
205      * Update Meter channel
206      *
207      * @param th Thing Handler instance
208      * @param profile ShellyDeviceProfile
209      * @param status Last ShellySettingsStatus
210      */
211     public static boolean updateMeters(ShellyThingInterface thingHandler, ShellySettingsStatus status) {
212         ShellyDeviceProfile profile = thingHandler.getProfile();
213
214         double accumulatedWatts = 0.0;
215         double accumulatedTotal = 0.0;
216         double accumulatedReturned = 0.0;
217
218         boolean updated = false;
219         // Devices without power meters get no updates
220         // We need to differ
221         // Roler+RGBW2 have multiple meters -> aggregate consumption to the functional device
222         // Meter and EMeter have a different set of channels
223         if (status.meters != null || status.emeters != null) {
224             if (!profile.isRoller && !profile.isRGBW2) {
225                 // In Relay mode we map eacher meter to the matching channel group
226                 int m = 0;
227                 if (!profile.isEMeter) {
228                     for (ShellySettingsMeter meter : status.meters) {
229                         if (m >= profile.numMeters) {
230                             // Shelly1: reports status.meters[0].is_valid = true, but even doesn't have a meter
231                             meter.isValid = false;
232                         }
233                         if (getBool(meter.isValid) || profile.isLight) { // RGBW2-white doesn't report valid flag
234                             // correctly in white mode
235                             String groupName = profile.getMeterGroup(m);
236                             if (!thingHandler.areChannelsCreated()) {
237                                 // skip for Shelly Bulb: JSON has a meter, but values don't get updated
238                                 if (!profile.isBulb) {
239                                     thingHandler.updateChannelDefinitions(ShellyChannelDefinitions
240                                             .createMeterChannels(thingHandler.getThing(), meter, groupName));
241                                 }
242                             }
243
244                             updated |= thingHandler.updateChannel(groupName, CHANNEL_METER_CURRENTWATTS,
245                                     toQuantityType(getDouble(meter.power), DIGITS_WATT, Units.WATT));
246                             accumulatedWatts += getDouble(meter.power);
247
248                             // convert Watt/Min to kw/h
249                             if (meter.total != null) {
250                                 double kwh = getDouble(meter.total) / 1000 / 60;
251                                 updated |= thingHandler.updateChannel(groupName, CHANNEL_METER_TOTALKWH,
252                                         toQuantityType(kwh, DIGITS_KWH, Units.KILOWATT_HOUR));
253                                 accumulatedTotal += kwh;
254                             }
255                             if (meter.counters != null) {
256                                 updated |= thingHandler.updateChannel(groupName, CHANNEL_METER_LASTMIN1,
257                                         toQuantityType(getDouble(meter.counters[0]), DIGITS_WATT, Units.WATT));
258                             }
259                             if (meter.timestamp != null) {
260                                 thingHandler.updateChannel(groupName, CHANNEL_LAST_UPDATE,
261                                         getTimestamp(getString(profile.settings.timezone), meter.timestamp));
262                             }
263                         }
264                         m++;
265                     }
266                 } else {
267                     for (ShellySettingsEMeter emeter : status.emeters) {
268                         if (getBool(emeter.isValid)) {
269                             String groupName = profile.getMeterGroup(m);
270                             if (!thingHandler.areChannelsCreated()) {
271                                 thingHandler.updateChannelDefinitions(ShellyChannelDefinitions
272                                         .createEMeterChannels(thingHandler.getThing(), profile, emeter, groupName));
273                             }
274
275                             // convert Watt/h to KW/h
276                             double total = getDouble(emeter.total) / 1000;
277                             double totalReturned = getDouble(emeter.totalReturned) / 1000;
278                             updated |= thingHandler.updateChannel(groupName, CHANNEL_METER_CURRENTWATTS,
279                                     toQuantityType(getDouble(emeter.power), DIGITS_WATT, Units.WATT));
280                             updated |= thingHandler.updateChannel(groupName, CHANNEL_METER_TOTALKWH,
281                                     toQuantityType(total, DIGITS_KWH, Units.KILOWATT_HOUR));
282                             updated |= thingHandler.updateChannel(groupName, CHANNEL_EMETER_TOTALRET,
283                                     toQuantityType(totalReturned, DIGITS_KWH, Units.KILOWATT_HOUR));
284                             updated |= thingHandler.updateChannel(groupName, CHANNEL_EMETER_REACTWATTS,
285                                     toQuantityType(getDouble(emeter.reactive), DIGITS_WATT, Units.WATT));
286                             updated |= thingHandler.updateChannel(groupName, CHANNEL_EMETER_VOLTAGE,
287                                     toQuantityType(getDouble(emeter.voltage), DIGITS_VOLT, Units.VOLT));
288                             updated |= thingHandler.updateChannel(groupName, CHANNEL_EMETER_CURRENT,
289                                     toQuantityType(getDouble(emeter.current), DIGITS_AMPERE, Units.AMPERE));
290                             updated |= thingHandler.updateChannel(groupName, CHANNEL_EMETER_PFACTOR,
291                                     toQuantityType(computePF(emeter), Units.PERCENT));
292
293                             accumulatedWatts += getDouble(emeter.power);
294                             accumulatedTotal += total;
295                             accumulatedReturned += totalReturned;
296                             if (updated) {
297                                 thingHandler.updateChannel(groupName, CHANNEL_LAST_UPDATE, getTimestamp());
298                             }
299                         }
300                         m++;
301                     }
302                 }
303             } else {
304                 // In Roller Mode we accumulate all meters to a single set of meters
305                 double currentWatts = 0.0;
306                 double totalWatts = 0.0;
307                 double lastMin1 = 0.0;
308                 long timestamp = 0l;
309                 String groupName = CHANNEL_GROUP_METER;
310
311                 if (!thingHandler.areChannelsCreated()) {
312                     ShellySettingsMeter m = status.meters.get(0);
313                     if (getBool(m.isValid)) {
314                         // Create channels for 1 Meter
315                         thingHandler.updateChannelDefinitions(
316                                 ShellyChannelDefinitions.createMeterChannels(thingHandler.getThing(), m, groupName));
317                     }
318                 }
319
320                 for (ShellySettingsMeter meter : status.meters) {
321                     if (getBool(meter.isValid)) {
322                         currentWatts += getDouble(meter.power);
323                         totalWatts += getDouble(meter.total);
324                         if (meter.counters != null) {
325                             lastMin1 += getDouble(meter.counters[0]);
326                         }
327                         if (getLong(meter.timestamp) > timestamp) {
328                             timestamp = getLong(meter.timestamp); // newest one
329                         }
330                     }
331                 }
332
333                 updated |= thingHandler.updateChannel(groupName, CHANNEL_METER_LASTMIN1,
334                         toQuantityType(getDouble(lastMin1), DIGITS_WATT, Units.WATT));
335
336                 // convert totalWatts into kw/h
337                 totalWatts = totalWatts / (60.0 * 1000.0);
338                 updated |= thingHandler.updateChannel(groupName, CHANNEL_METER_CURRENTWATTS,
339                         toQuantityType(currentWatts, DIGITS_WATT, Units.WATT));
340                 updated |= thingHandler.updateChannel(groupName, CHANNEL_METER_TOTALKWH,
341                         toQuantityType(totalWatts, DIGITS_KWH, Units.KILOWATT_HOUR));
342
343                 if (updated && timestamp > 0) {
344                     thingHandler.updateChannel(groupName, CHANNEL_LAST_UPDATE,
345                             getTimestamp(getString(profile.settings.timezone), timestamp));
346                 }
347             }
348
349             if (!profile.isRoller && !profile.isRGBW2) {
350                 thingHandler.updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_ACCUWATTS, toQuantityType(
351                         status.totalPower != null ? status.totalPower : accumulatedWatts, DIGITS_WATT, Units.WATT));
352                 thingHandler.updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_ACCUTOTAL,
353                         toQuantityType(status.totalCurrent != null ? status.totalCurrent / 1000 : accumulatedTotal,
354                                 DIGITS_KWH, Units.KILOWATT_HOUR));
355                 thingHandler.updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_ACCURETURNED,
356                         toQuantityType(status.totalReturned != null ? status.totalReturned / 1000 : accumulatedReturned,
357                                 DIGITS_KWH, Units.KILOWATT_HOUR));
358             }
359         }
360
361         return updated;
362     }
363
364     private static Double computePF(ShellySettingsEMeter emeter) {
365         if (emeter.pf != null) { // EM3
366             return emeter.pf; // take device value
367         }
368
369         // EM: compute from provided values
370         if (emeter.reactive != null && Math.abs(emeter.power) + Math.abs(emeter.reactive) > 1.5) {
371             double pf = emeter.power / Math.sqrt(emeter.power * emeter.power + emeter.reactive * emeter.reactive);
372             return pf;
373         }
374         return 0.0;
375     }
376
377     /**
378      * Update Sensor channel
379      *
380      * @param th Thing Handler instance
381      * @param profile ShellyDeviceProfile
382      * @param status Last ShellySettingsStatus
383      *
384      * @throws ShellyApiException
385      */
386     public static boolean updateSensors(ShellyThingInterface thingHandler, ShellySettingsStatus status)
387             throws ShellyApiException {
388         ShellyDeviceProfile profile = thingHandler.getProfile();
389
390         boolean updated = false;
391         if (profile.isSensor || profile.hasBattery) {
392             ShellyStatusSensor sdata = thingHandler.getApi().getSensorStatus();
393             if (!thingHandler.areChannelsCreated()) {
394                 thingHandler.updateChannelDefinitions(
395                         ShellyChannelDefinitions.createSensorChannels(thingHandler.getThing(), profile, sdata));
396             }
397
398             updated |= thingHandler.updateWakeupReason(sdata.actReasons);
399
400             if ((sdata.sensor != null) && sdata.sensor.isValid) {
401                 // Shelly DW: “sensor”:{“state”:“open”, “is_valid”:true},
402                 updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_STATE,
403                         getOpenClosed(getString(sdata.sensor.state).equalsIgnoreCase(SHELLY_API_DWSTATE_OPEN)));
404                 String sensorError = sdata.sensorError;
405                 boolean changed = thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_ERROR,
406                         getStringType(sensorError));
407                 if (changed && !"0".equals(sensorError)) {
408                     thingHandler.postEvent(getString(sdata.sensorError), true);
409                 }
410                 updated |= changed;
411             }
412             if (sdata.tmp != null && getBool(sdata.tmp.isValid)) {
413                 Double temp = getString(sdata.tmp.units).toUpperCase().equals(SHELLY_TEMP_CELSIUS)
414                         ? getDouble(sdata.tmp.tC)
415                         : getDouble(sdata.tmp.tF);
416                 updated |= updateTempChannel(thingHandler, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_TEMP,
417                         temp.doubleValue(), getString(sdata.tmp.units));
418             } else if (status.thermostats != null) {
419                 // Shelly TRV
420                 if (profile.settings.thermostats != null) {
421                     ShellyThermnostat ps = profile.settings.thermostats.get(0);
422                     ShellyThermnostat t = status.thermostats.get(0);
423                     int bminutes = getInteger(t.boostMinutes) >= 0 ? getInteger(t.boostMinutes)
424                             : getInteger(ps.boostMinutes);
425                     updated |= thingHandler.updateChannel(CHANNEL_GROUP_CONTROL, CHANNEL_CONTROL_BCONTROL,
426                             getOnOff(getInteger(t.boostMinutes) > 0));
427                     updated |= thingHandler.updateChannel(CHANNEL_GROUP_CONTROL, CHANNEL_CONTROL_BTIMER,
428                             toQuantityType((double) bminutes, DIGITS_NONE, Units.MINUTE));
429                     updated |= thingHandler.updateChannel(CHANNEL_GROUP_CONTROL, CHANNEL_CONTROL_MODE, getStringType(
430                             getBool(t.targetTemp.enabled) ? SHELLY_TRV_MODE_AUTO : SHELLY_TRV_MODE_MANUAL));
431                     updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_OPEN,
432                             getOpenClosed(t.windowOpen));
433
434                     int pid = getBool(t.schedule) ? getInteger(t.profile) : 0;
435                     updated |= thingHandler.updateChannel(CHANNEL_GROUP_CONTROL, CHANNEL_CONTROL_SCHEDULE,
436                             getOnOff(t.schedule));
437                     updated |= thingHandler.updateChannel(CHANNEL_GROUP_CONTROL, CHANNEL_CONTROL_PROFILE,
438                             getStringType(profile.getValueProfile(0, pid)));
439                     if (t.tmp != null) {
440                         updated |= updateTempChannel(thingHandler, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_TEMP,
441                                 t.tmp.value, t.tmp.units);
442                         updated |= updateTempChannel(thingHandler, CHANNEL_GROUP_SENSOR, CHANNEL_CONTROL_SETTEMP,
443                                 t.targetTemp.value, t.targetTemp.unit);
444                     }
445                     if (t.pos != null) {
446                         updated |= thingHandler.updateChannel(CHANNEL_GROUP_CONTROL, CHANNEL_CONTROL_POSITION,
447                                 t.pos != -1 ? toQuantityType(t.pos, DIGITS_NONE, Units.PERCENT) : UnDefType.UNDEF);
448                         updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_STATE,
449                                 getOpenClosed(getDouble(t.pos) > 0));
450                     }
451                 }
452             }
453
454             if (sdata.hum != null) {
455                 updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_HUM,
456                         toQuantityType(getDouble(sdata.hum.value), DIGITS_PERCENT, Units.PERCENT));
457             }
458             if ((sdata.lux != null) && getBool(sdata.lux.isValid)) {
459                 // “lux”:{“value”:30, “illumination”: “dark”, “is_valid”:true},
460                 updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_LUX,
461                         toQuantityType(getDouble(sdata.lux.value), DIGITS_LUX, Units.LUX));
462                 if (sdata.lux.illumination != null) {
463                     updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_ILLUM,
464                             getStringType(sdata.lux.illumination));
465                 }
466             }
467             if (sdata.accel != null) {
468                 updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_TILT,
469                         toQuantityType(getDouble(sdata.accel.tilt.doubleValue()), DIGITS_NONE, Units.DEGREE_ANGLE));
470             }
471             if (sdata.flood != null) {
472                 updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_FLOOD,
473                         getOnOff(sdata.flood));
474             }
475             if (sdata.smoke != null) {
476                 updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_SMOKE,
477                         getOnOff(sdata.smoke));
478             }
479             if (sdata.mute != null) {
480                 updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_MUTE, getOnOff(sdata.mute));
481             }
482
483             if (sdata.gasSensor != null) {
484                 updated |= thingHandler.updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_SELFTTEST,
485                         getStringType(sdata.gasSensor.selfTestState));
486                 updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_ALARM_STATE,
487                         getStringType(sdata.gasSensor.alarmState));
488                 updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_SSTATE,
489                         getStringType(sdata.gasSensor.sensorState));
490             }
491             if ((sdata.concentration != null) && sdata.concentration.isValid) {
492                 updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_PPM, toQuantityType(
493                         getInteger(sdata.concentration.ppm).doubleValue(), DIGITS_NONE, Units.PARTS_PER_MILLION));
494             }
495             if ((sdata.adcs != null) && (sdata.adcs.size() > 0)) {
496                 ShellyADC adc = sdata.adcs.get(0);
497                 updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_VOLTAGE,
498                         toQuantityType(getDouble(adc.voltage), 2, Units.VOLT));
499             }
500
501             boolean charger = (getInteger(profile.settings.externalPower) == 1) || getBool(sdata.charger);
502             if ((profile.settings.externalPower != null) || (sdata.charger != null)) {
503                 updated |= thingHandler.updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_CHARGER,
504                         getOnOff(charger));
505             }
506             if (sdata.bat != null) { // no update for Sense
507                 updated |= thingHandler.updateChannel(CHANNEL_GROUP_BATTERY, CHANNEL_SENSOR_BAT_LEVEL,
508                         toQuantityType(getDouble(sdata.bat.value), 0, Units.PERCENT));
509
510                 int lowBattery = thingHandler.getThingConfig().lowBattery;
511                 boolean changed = thingHandler.updateChannel(CHANNEL_GROUP_BATTERY, CHANNEL_SENSOR_BAT_LOW,
512                         getOnOff(!charger && getDouble(sdata.bat.value) < lowBattery));
513                 updated |= changed;
514                 if (!charger && changed && getDouble(sdata.bat.value) < lowBattery) {
515                     thingHandler.postEvent(ALARM_TYPE_LOW_BATTERY, false);
516                 }
517             }
518
519             if (sdata.motion != null) { // Shelly Sense
520                 updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_MOTION,
521                         getOnOff(sdata.motion));
522             }
523             if (sdata.sensor != null) { // Shelly Motion
524                 updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_MOTION_ACT,
525                         getOnOff(sdata.sensor.motionActive));
526                 updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_MOTION,
527                         getOnOff(sdata.sensor.motion));
528                 long timestamp = getLong(sdata.sensor.motionTimestamp);
529                 if (timestamp != 0) {
530                     updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_MOTION_TS,
531                             getTimestamp(getString(profile.settings.timezone), timestamp));
532                 }
533             }
534
535             updated |= thingHandler.updateInputs(status);
536
537             if (updated) {
538                 thingHandler.updateChannel(profile.getControlGroup(0), CHANNEL_LAST_UPDATE, getTimestamp());
539             }
540         }
541         return updated;
542     }
543
544     public static boolean updateDimmers(ShellyThingInterface thingHandler, ShellySettingsStatus orgStatus)
545             throws ShellyApiException {
546         boolean updated = false;
547         ShellyDeviceProfile profile = thingHandler.getProfile();
548         if (profile.isDimmer) {
549             // We need to fixup the returned Json: The dimmer returns light[] element, which is ok, but it doesn't have
550             // the same structure as lights[] from Bulb,RGBW2 and Duo. The tag gets replaced by dimmers[] so that Gson
551             // maps to a different structure (ShellyShortLight).
552             Gson gson = new Gson();
553             ShellySettingsStatus dstatus = !profile.isGen2
554                     ? fromJson(gson, Shelly1ApiJsonDTO.fixDimmerJson(orgStatus.json), ShellySettingsStatus.class)
555                     : orgStatus;
556
557             int l = 0;
558             for (ShellyShortLightStatus dimmer : dstatus.dimmers) {
559                 Integer r = l + 1;
560                 String groupName = profile.numRelays <= 1 ? CHANNEL_GROUP_DIMMER_CONTROL
561                         : CHANNEL_GROUP_DIMMER_CONTROL + r.toString();
562
563                 if (!thingHandler.areChannelsCreated()) {
564                     thingHandler.updateChannelDefinitions(ShellyChannelDefinitions
565                             .createDimmerChannels(thingHandler.getThing(), profile, dstatus, l));
566                 }
567
568                 ShellySettingsDimmer ds = profile.settings.dimmers.get(l);
569                 if (ds.name != null) {
570                     updated |= thingHandler.updateChannel(groupName, CHANNEL_OUTPUT_NAME, getStringType(ds.name));
571                 }
572
573                 // On a status update we map a dimmer.ison = false to brightness 0 rather than the device's brightness
574                 // and send an OFF status to the same channel.
575                 // When the device's brightness is > 0 we send the new value to the channel and an ON command
576                 if (dimmer.ison) {
577                     updated |= thingHandler.updateChannel(groupName, CHANNEL_BRIGHTNESS + "$Switch", OnOffType.ON);
578                     updated |= thingHandler.updateChannel(groupName, CHANNEL_BRIGHTNESS + "$Value",
579                             toQuantityType((double) getInteger(dimmer.brightness), DIGITS_NONE, Units.PERCENT));
580                 } else {
581                     updated |= thingHandler.updateChannel(groupName, CHANNEL_BRIGHTNESS + "$Switch", OnOffType.OFF);
582                     updated |= thingHandler.updateChannel(groupName, CHANNEL_BRIGHTNESS + "$Value",
583                             toQuantityType(0.0, DIGITS_NONE, Units.PERCENT));
584                 }
585
586                 if (profile.settings.dimmers != null) {
587                     ShellySettingsDimmer dsettings = profile.settings.dimmers.get(l);
588                     if (dsettings != null) {
589                         updated |= thingHandler.updateChannel(groupName, CHANNEL_TIMER_AUTOON,
590                                 toQuantityType(getDouble(dsettings.autoOn), Units.SECOND));
591                         updated |= thingHandler.updateChannel(groupName, CHANNEL_TIMER_AUTOOFF,
592                                 toQuantityType(getDouble(dsettings.autoOff), Units.SECOND));
593                     }
594                 }
595
596                 l++;
597             }
598         }
599         return updated;
600     }
601
602     public static boolean updateTempChannel(@Nullable ShellyShortTemp sensor, ShellyThingInterface thingHandler,
603             String channel) {
604         return sensor != null ? updateTempChannel(thingHandler, CHANNEL_GROUP_SENSOR, channel, sensor.tC, "") : false;
605     }
606
607     public static boolean updateTempChannel(ShellyThingInterface thingHandler, String group, String channel,
608             @Nullable Double temp, @Nullable String unit) {
609         if (temp == null || temp == SHELLY_API_INVTEMP) {
610             return false;
611         }
612         return thingHandler.updateChannel(group, channel,
613                 toQuantityType(convertToC(temp, unit), DIGITS_TEMP, SIUnits.CELSIUS));
614     }
615
616     private static Double convertToC(@Nullable Double temp, @Nullable String unit) {
617         if (temp == null || temp == SHELLY_API_INVTEMP) {
618             return SHELLY_API_INVTEMP;
619         }
620         if (SHELLY_TEMP_FAHRENHEIT.equalsIgnoreCase(getString(unit))) {
621             // convert Fahrenheit to Celsius
622             return ImperialUnits.FAHRENHEIT.getConverterTo(SIUnits.CELSIUS).convert(temp).doubleValue();
623         }
624         return temp;
625     }
626 }