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