]> git.basschouten.com Git - openhab-addons.git/blob
95f00e6539701eec81a6b6a269489d9d0e816026
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2024 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 thingHandler Thing Handler instance
58      * @param status Status message
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 && 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));
85         }
86         thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_SLEEPTIME,
87                 toQuantityType(getInteger(status.sleepTime), Units.SECOND));
88
89         thingHandler.updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_UPDATE, getOnOff(status.hasUpdate));
90
91         if (profile.settings.calibrated != null) {
92             thingHandler.updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_CALIBRATED,
93                     getOnOff(profile.settings.calibrated));
94         }
95
96         return false; // device status never triggers update
97     }
98
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);
105         } else {
106             throw new IllegalArgumentException("No relay settings");
107         }
108
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));
113
114             if (getBool(relay.overpower)) {
115                 thingHandler.postEvent(ALARM_TYPE_OVERPOWER, false);
116             }
117
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));
124                 }
125             }
126
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));
132         }
133         return updated;
134     }
135
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));
144             }
145
146             String state = getString(control.state);
147             int pos = -1;
148             switch (state) {
149                 case SHELLY_ALWD_ROLLER_TURN_OPEN:
150                     pos = SHELLY_MAX_ROLLER_POS;
151                     break;
152                 case SHELLY_ALWD_ROLLER_TURN_CLOSE:
153                     pos = SHELLY_MIN_ROLLER_POS;
154                     break;
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));
159                     }
160                     break;
161             }
162             if (pos != -1) {
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));
169             }
170
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));
176         }
177         return updated;
178     }
179
180     /**
181      * Update Meter channel
182      *
183      * @param thingHandler Thing Handler instance
184      * @param status Last ShellySettingsStatus
185      */
186     public static boolean updateMeters(ShellyThingInterface thingHandler, ShellySettingsStatus status) {
187         ShellyDeviceProfile profile = thingHandler.getProfile();
188
189         double accumulatedWatts = 0.0;
190         double accumulatedTotal = 0.0;
191         double accumulatedReturned = 0.0;
192
193         boolean updated = false;
194         // Devices without power meters get no updates
195         // We need to differ
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
201                 int m = 0;
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;
207                         }
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));
216                                 }
217                             }
218
219                             updated |= thingHandler.updateChannel(groupName, CHANNEL_METER_CURRENTWATTS,
220                                     toQuantityType(getDouble(meter.power), DIGITS_WATT, Units.WATT));
221                             accumulatedWatts += getDouble(meter.power);
222
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;
229                             }
230                             if (meter.counters != null) {
231                                 updated |= thingHandler.updateChannel(groupName, CHANNEL_METER_LASTMIN1,
232                                         toQuantityType(getDouble(meter.counters[0]), DIGITS_WATT, Units.WATT));
233                             }
234                             if (meter.timestamp != null) {
235                                 thingHandler.updateChannel(groupName, CHANNEL_LAST_UPDATE,
236                                         getTimestamp(getString(profile.settings.timezone), meter.timestamp));
237                             }
238                         }
239                         m++;
240                     }
241                 } else {
242                     if (status.neutralCurrent != null) {
243                         if (!thingHandler.areChannelsCreated()) {
244                             thingHandler.updateChannelDefinitions(ShellyChannelDefinitions.createEMNCurrentChannels(
245                                     thingHandler.getThing(), profile.settings.neutralCurrent, status.neutralCurrent));
246                         }
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));
258                         }
259                     }
260
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));
267                             }
268
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));
286
287                             accumulatedWatts += getDouble(emeter.power);
288                             accumulatedTotal += total;
289                             accumulatedReturned += totalReturned;
290                             if (updated) {
291                                 thingHandler.updateChannel(groupName, CHANNEL_LAST_UPDATE, getTimestamp());
292                             }
293                         }
294                         m++;
295                     }
296                 }
297             } else {
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;
302                 long timestamp = 0l;
303                 String groupName = CHANNEL_GROUP_METER;
304
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));
311                     }
312                 }
313
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]);
320                         }
321                         if (getLong(meter.timestamp) > timestamp) {
322                             timestamp = getLong(meter.timestamp); // newest one
323                         }
324                     }
325                 }
326
327                 updated |= thingHandler.updateChannel(groupName, CHANNEL_METER_LASTMIN1,
328                         toQuantityType(getDouble(lastMin1), DIGITS_WATT, Units.WATT));
329
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));
336
337                 if (updated && timestamp > 0) {
338                     thingHandler.updateChannel(groupName, CHANNEL_LAST_UPDATE,
339                             getTimestamp(getString(profile.settings.timezone), timestamp));
340                 }
341             }
342
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));
352             }
353         }
354
355         return updated;
356     }
357
358     private static Double computePF(ShellySettingsEMeter emeter) {
359         if (emeter.pf != null) { // EM3
360             return emeter.pf; // take device value
361         }
362
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);
366         }
367         return 0.0;
368     }
369
370     /**
371      * Update Sensor channel
372      *
373      * @param thingHandler Thing Handler instance
374      * @param status Last ShellySettingsStatus
375      *
376      * @throws ShellyApiException
377      */
378     public static boolean updateSensors(ShellyThingInterface thingHandler, ShellySettingsStatus status)
379             throws ShellyApiException {
380         ShellyDeviceProfile profile = thingHandler.getProfile();
381
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));
388             }
389
390             updated |= thingHandler.updateWakeupReason(sdata.actReasons);
391
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);
401                 }
402                 updated |= changed;
403             }
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) {
411                 // Shelly TRV
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));
423
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)));
429                     if (t.tmp != null) {
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;
434                         }
435                         updated |= updateTempChannel(thingHandler, CHANNEL_GROUP_CONTROL, CHANNEL_CONTROL_SETTEMP,
436                                 t.targetTemp.value, t.targetTemp.unit);
437                     }
438                     if (t.pos != null) {
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));
443                     }
444                 }
445             }
446
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));
450             }
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));
458                 }
459             }
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));
463             }
464             if (sdata.flood != null) {
465                 updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_FLOOD,
466                         getOnOff(sdata.flood));
467             }
468             if (sdata.smoke != null) {
469                 updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_SMOKE,
470                         getOnOff(sdata.smoke));
471             }
472             if (sdata.mute != null) {
473                 updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_MUTE, getOnOff(sdata.mute));
474             }
475
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));
483             }
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));
487             }
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));
492             }
493
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,
497                         getOnOff(charger));
498             }
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));
502
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));
506                 updated |= changed;
507                 if (!charger && changed && getDouble(sdata.bat.value) < lowBattery) {
508                     thingHandler.postEvent(ALARM_TYPE_LOW_BATTERY, false);
509                 }
510             }
511
512             if (sdata.motion != null) { // Shelly Sense
513                 updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_MOTION,
514                         getOnOff(sdata.motion));
515             }
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));
525                 }
526             }
527
528             updated |= thingHandler.updateInputs(status);
529
530             if (updated) {
531                 thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_LAST_UPDATE, getTimestamp());
532             }
533         }
534
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);
544         }
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));
548         }
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));
552         }
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));
556         }
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));
560         }
561
562         return updated;
563     }
564
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)
576                     : orgStatus;
577
578             int l = 0;
579             for (ShellyShortLightStatus dimmer : dstatus.dimmers) {
580                 Integer r = l + 1;
581                 String groupName = profile.numRelays <= 1 ? CHANNEL_GROUP_DIMMER_CONTROL
582                         : CHANNEL_GROUP_DIMMER_CONTROL + r.toString();
583
584                 if (!thingHandler.areChannelsCreated()) {
585                     thingHandler.updateChannelDefinitions(ShellyChannelDefinitions
586                             .createDimmerChannels(thingHandler.getThing(), profile, dstatus, l));
587                 }
588
589                 ShellySettingsDimmer ds = profile.settings.dimmers.get(l);
590                 if (ds.name != null) {
591                     updated |= thingHandler.updateChannel(groupName, CHANNEL_OUTPUT_NAME, getStringType(ds.name));
592                 }
593
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) {
598                     updated |= thingHandler.updateChannel(groupName, CHANNEL_BRIGHTNESS + "$Switch", OnOffType.ON);
599                     updated |= thingHandler.updateChannel(groupName, CHANNEL_BRIGHTNESS + "$Value",
600                             toQuantityType((double) getInteger(dimmer.brightness), DIGITS_NONE, Units.PERCENT));
601                 } else {
602                     updated |= thingHandler.updateChannel(groupName, CHANNEL_BRIGHTNESS + "$Switch", OnOffType.OFF);
603                     updated |= thingHandler.updateChannel(groupName, CHANNEL_BRIGHTNESS + "$Value",
604                             toQuantityType(0.0, DIGITS_NONE, Units.PERCENT));
605                 }
606
607                 if (profile.settings.dimmers != null) {
608                     ShellySettingsDimmer dsettings = profile.settings.dimmers.get(l);
609                     if (dsettings != null) {
610                         updated |= thingHandler.updateChannel(groupName, CHANNEL_TIMER_AUTOON,
611                                 toQuantityType(getDouble(dsettings.autoOn), Units.SECOND));
612                         updated |= thingHandler.updateChannel(groupName, CHANNEL_TIMER_AUTOOFF,
613                                 toQuantityType(getDouble(dsettings.autoOff), Units.SECOND));
614                     }
615                 }
616
617                 l++;
618             }
619         }
620         return updated;
621     }
622
623     public static boolean updateTempChannel(@Nullable ShellyShortTemp sensor, ShellyThingInterface thingHandler,
624             String channel) {
625         return sensor != null ? updateTempChannel(thingHandler, CHANNEL_GROUP_SENSOR, channel, sensor.tC, "") : false;
626     }
627
628     public static boolean updateTempChannel(ShellyThingInterface thingHandler, String group, String channel,
629             @Nullable Double temp, @Nullable String unit) {
630         if (temp == null || temp == SHELLY_API_INVTEMP) {
631             return false;
632         }
633         return thingHandler.updateChannel(group, channel,
634                 toQuantityType(convertToC(temp, unit), DIGITS_TEMP, SIUnits.CELSIUS));
635     }
636
637     private static Double convertToC(@Nullable Double temp, @Nullable String unit) {
638         if (temp == null || temp == SHELLY_API_INVTEMP) {
639             return SHELLY_API_INVTEMP;
640         }
641         if (SHELLY_TEMP_FAHRENHEIT.equalsIgnoreCase(getString(unit))) {
642             // convert Fahrenheit to Celsius
643             return ImperialUnits.FAHRENHEIT.getConverterTo(SIUnits.CELSIUS).convert(temp).doubleValue();
644         }
645         return temp;
646     }
647 }