]> git.basschouten.com Git - openhab-addons.git/blob
2805d1310107f4f6969c97c00a5e357eae3e90fc
[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.api2;
14
15 import static org.openhab.binding.shelly.internal.ShellyBindingConstants.CHANNEL_INPUT;
16 import static org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.*;
17 import static org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.*;
18 import static org.openhab.binding.shelly.internal.util.ShellyUtils.*;
19
20 import java.util.ArrayList;
21 import java.util.HashMap;
22 import java.util.Map;
23 import java.util.Random;
24
25 import org.eclipse.jdt.annotation.NonNullByDefault;
26 import org.eclipse.jdt.annotation.Nullable;
27 import org.eclipse.jetty.client.HttpClient;
28 import org.openhab.binding.shelly.internal.api.ShellyApiException;
29 import org.openhab.binding.shelly.internal.api.ShellyDeviceProfile;
30 import org.openhab.binding.shelly.internal.api.ShellyHttpClient;
31 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyFavPos;
32 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyInputState;
33 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyRollerStatus;
34 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySensorTmp;
35 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsDimmer;
36 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsEMeter;
37 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsInput;
38 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsMeter;
39 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsRelay;
40 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsRoller;
41 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsStatus;
42 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyShortLightStatus;
43 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyShortStatusRelay;
44 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyStatusRelay;
45 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyStatusSensor;
46 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyStatusSensor.ShellyExtAnalogInput;
47 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyStatusSensor.ShellyExtDigitalInput;
48 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyStatusSensor.ShellyExtHumidity;
49 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyStatusSensor.ShellyExtTemperature;
50 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyStatusSensor.ShellyExtTemperature.ShellyShortTemp;
51 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyStatusSensor.ShellyExtVoltage;
52 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyStatusSensor.ShellySensorBat;
53 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyStatusSensor.ShellySensorHum;
54 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyStatusSensor.ShellySensorLux;
55 import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2AuthRsp;
56 import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2DeviceConfig.Shelly2DevConfigCover;
57 import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2DeviceConfig.Shelly2DevConfigInput;
58 import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2DeviceConfig.Shelly2DevConfigSwitch;
59 import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2DeviceConfig.Shelly2GetConfigResult;
60 import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2DeviceStatus.Shelly2DeviceStatusLight;
61 import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2DeviceStatus.Shelly2DeviceStatusResult;
62 import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2DeviceStatus.Shelly2DeviceStatusResult.Shelly2CoverStatus;
63 import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2DeviceStatus.Shelly2DeviceStatusResult.Shelly2DeviceStatusEm;
64 import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2DeviceStatus.Shelly2DeviceStatusResult.Shelly2DeviceStatusHumidity;
65 import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2DeviceStatus.Shelly2DeviceStatusResult.Shelly2DeviceStatusIlluminance;
66 import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2DeviceStatus.Shelly2DeviceStatusResult.Shelly2DeviceStatusPower;
67 import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2DeviceStatus.Shelly2DeviceStatusResult.Shelly2DeviceStatusSmoke;
68 import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2DeviceStatus.Shelly2DeviceStatusResult.Shelly2DeviceStatusTempId;
69 import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2DeviceStatus.Shelly2InputStatus;
70 import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2RelayStatus;
71 import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2RpcBaseMessage;
72 import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2StatusEm1;
73 import org.openhab.binding.shelly.internal.config.ShellyThingConfiguration;
74 import org.openhab.binding.shelly.internal.handler.ShellyBaseHandler;
75 import org.openhab.binding.shelly.internal.handler.ShellyComponents;
76 import org.openhab.binding.shelly.internal.handler.ShellyThingInterface;
77 import org.openhab.core.types.State;
78 import org.slf4j.Logger;
79 import org.slf4j.LoggerFactory;
80
81 /**
82  * {@link Shelly2ApiClient} Low level part of the RPC API
83  *
84  * @author Markus Michels - Initial contribution
85  */
86 @NonNullByDefault
87 public class Shelly2ApiClient extends ShellyHttpClient {
88     private final Logger logger = LoggerFactory.getLogger(Shelly2ApiClient.class);
89     protected final Random random = new Random();
90     protected final ShellyStatusRelay relayStatus = new ShellyStatusRelay();
91     protected final ShellyStatusSensor sensorData = new ShellyStatusSensor();
92     protected final ArrayList<ShellyRollerStatus> rollerStatus = new ArrayList<>();
93     protected @Nullable ShellyThingInterface thing;
94     protected @Nullable Shelly2AuthRsp authReq;
95
96     public Shelly2ApiClient(String thingName, ShellyThingInterface thing) {
97         super(thingName, thing);
98         this.thing = thing;
99     }
100
101     public Shelly2ApiClient(String thingName, ShellyThingConfiguration config, HttpClient httpClient) {
102         super(thingName, config, httpClient);
103     }
104
105     protected static final Map<String, String> MAP_INMODE_BTNTYPE = new HashMap<>();
106     static {
107         MAP_INMODE_BTNTYPE.put(SHELLY2_BTNT_MOMENTARY, SHELLY_BTNT_MOMENTARY);
108         MAP_INMODE_BTNTYPE.put(SHELLY2_BTNT_FLIP, SHELLY_BTNT_TOGGLE);
109         MAP_INMODE_BTNTYPE.put(SHELLY2_BTNT_FOLLOW, SHELLY_BTNT_EDGE);
110         MAP_INMODE_BTNTYPE.put(SHELLY2_BTNT_DETACHED, SHELLY_BTNT_MOMENTARY);
111     }
112
113     protected static final Map<String, String> MAP_INPUT_EVENT_TYPE = new HashMap<>();
114     static {
115         MAP_INPUT_EVENT_TYPE.put(SHELLY2_EVENT_1PUSH, SHELLY_BTNEVENT_1SHORTPUSH);
116         MAP_INPUT_EVENT_TYPE.put(SHELLY2_EVENT_2PUSH, SHELLY_BTNEVENT_2SHORTPUSH);
117         MAP_INPUT_EVENT_TYPE.put(SHELLY2_EVENT_3PUSH, SHELLY_BTNEVENT_3SHORTPUSH);
118         MAP_INPUT_EVENT_TYPE.put(SHELLY2_EVENT_LPUSH, SHELLY_BTNEVENT_LONGPUSH);
119         MAP_INPUT_EVENT_TYPE.put(SHELLY2_EVENT_LSPUSH, SHELLY_BTNEVENT_LONGSHORTPUSH);
120         MAP_INPUT_EVENT_TYPE.put(SHELLY2_EVENT_SLPUSH, SHELLY_BTNEVENT_SHORTLONGPUSH);
121     }
122
123     protected static final Map<String, String> MAP_INPUT_EVENT_ID = new HashMap<>();
124     static {
125         MAP_INPUT_EVENT_ID.put(SHELLY2_EVENT_BTNUP, SHELLY_EVENT_BTN_OFF);
126         MAP_INPUT_EVENT_ID.put(SHELLY2_EVENT_BTNDOWN, SHELLY_EVENT_BTN_ON);
127         MAP_INPUT_EVENT_ID.put(SHELLY2_EVENT_1PUSH, SHELLY_EVENT_SHORTPUSH);
128         MAP_INPUT_EVENT_ID.put(SHELLY2_EVENT_2PUSH, SHELLY_EVENT_DOUBLE_SHORTPUSH);
129         MAP_INPUT_EVENT_ID.put(SHELLY2_EVENT_3PUSH, SHELLY_EVENT_TRIPLE_SHORTPUSH);
130         MAP_INPUT_EVENT_ID.put(SHELLY2_EVENT_LPUSH, SHELLY_EVENT_LONGPUSH);
131         MAP_INPUT_EVENT_ID.put(SHELLY2_EVENT_LSPUSH, SHELLY_EVENT_LONG_SHORTPUSH);
132         MAP_INPUT_EVENT_ID.put(SHELLY2_EVENT_SLPUSH, SHELLY_EVENT_SHORT_LONGTPUSH);
133     }
134
135     protected static final Map<String, String> MAP_INPUT_MODE = new HashMap<>();
136     static {
137         MAP_INPUT_MODE.put(SHELLY2_RMODE_SINGLE, SHELLY_INP_MODE_ONEBUTTON);
138         MAP_INPUT_MODE.put(SHELLY2_RMODE_DUAL, SHELLY_INP_MODE_OPENCLOSE);
139         MAP_INPUT_MODE.put(SHELLY2_RMODE_DETACHED, SHELLY_INP_MODE_ONEBUTTON);
140     }
141
142     protected static final Map<String, String> MAP_ROLLER_STATE = new HashMap<>();
143     static {
144         MAP_ROLLER_STATE.put(SHELLY2_RSTATE_OPEN, SHELLY_RSTATE_OPEN);
145         MAP_ROLLER_STATE.put(SHELLY2_RSTATE_CLOSED, SHELLY_RSTATE_CLOSE);
146         MAP_ROLLER_STATE.put(SHELLY2_RSTATE_OPENING, SHELLY2_RSTATE_OPENING); // Gen2-only
147         MAP_ROLLER_STATE.put(SHELLY2_RSTATE_CLOSING, SHELLY2_RSTATE_CLOSING); // Gen2-only
148         MAP_ROLLER_STATE.put(SHELLY2_RSTATE_STOPPED, SHELLY_RSTATE_STOP);
149         MAP_ROLLER_STATE.put(SHELLY2_RSTATE_CALIB, SHELLY2_RSTATE_CALIB); // Gen2-only
150     }
151     protected static final Map<String, String> MAP_PROFILE = new HashMap<>();
152     static {
153         MAP_PROFILE.put(SHELLY_CLASS_RELAY, SHELLY2_PROFILE_RELAY);
154         MAP_PROFILE.put(SHELLY_CLASS_ROLLER, SHELLY2_PROFILE_COVER);
155     }
156
157     protected @Nullable ArrayList<@Nullable ShellySettingsRelay> fillRelaySettings(ShellyDeviceProfile profile,
158             Shelly2GetConfigResult dc) {
159         ArrayList<@Nullable ShellySettingsRelay> relays = new ArrayList<>();
160         addRelaySettings(relays, dc.switch0);
161         addRelaySettings(relays, dc.switch1);
162         addRelaySettings(relays, dc.switch2);
163         addRelaySettings(relays, dc.switch3);
164         addRelaySettings(relays, dc.switch100);
165         return !relays.isEmpty() ? relays : null;
166     }
167
168     private void addRelaySettings(ArrayList<@Nullable ShellySettingsRelay> relays,
169             @Nullable Shelly2DevConfigSwitch cs) {
170         if (cs == null) {
171             return;
172         }
173
174         ShellySettingsRelay rsettings = new ShellySettingsRelay();
175         rsettings.id = cs.id;
176         rsettings.isValid = cs.id != null;
177         rsettings.name = cs.name;
178         rsettings.ison = false;
179         rsettings.autoOn = getBool(cs.autoOn) ? cs.autoOnDelay : 0;
180         rsettings.autoOff = getBool(cs.autoOff) ? cs.autoOffDelay : 0;
181         rsettings.hasTimer = false;
182         rsettings.btnType = mapValue(MAP_INMODE_BTNTYPE, getString(cs.mode).toLowerCase());
183         relays.add(rsettings);
184     }
185
186     protected boolean fillDeviceStatus(ShellySettingsStatus status, Shelly2DeviceStatusResult result,
187             boolean channelUpdate) throws ShellyApiException {
188         boolean updated = false;
189
190         if (result.temperature0 != null && result.temperature0.tC != null && !getProfile().isSensor) {
191             if (status.tmp == null) {
192                 status.tmp = new ShellySensorTmp();
193             }
194             status.temperature = status.tmp.tC = result.temperature0.tC;
195         }
196
197         updated |= updateInputStatus(status, result, channelUpdate);
198         updated |= updateRelayStatus(status, result.switch0, channelUpdate);
199         updated |= updateRelayStatus(status, result.switch1, channelUpdate);
200         updated |= updateRelayStatus(status, result.switch2, channelUpdate);
201         updated |= updateRelayStatus(status, result.switch3, channelUpdate);
202         updated |= updateRelayStatus(status, result.switch100, channelUpdate);
203         updated |= updateRelayStatus(status, result.pm10, channelUpdate);
204         updated |= updateEmStatus(status, result.em0, channelUpdate);
205         updated |= updateEmStatus(status, result.em10, channelUpdate);
206         updated |= updateEmStatus(status, result.em11, channelUpdate);
207         updated |= updateRollerStatus(status, result.cover0, channelUpdate);
208         updated |= updateDimmerStatus(status, result.light0, channelUpdate);
209         if (channelUpdate) {
210             updated |= ShellyComponents.updateMeters(getThing(), status);
211         }
212
213         updateHumidityStatus(sensorData, result.humidity0);
214         updateTemperatureStatus(sensorData, result.temperature0);
215         updateIlluminanceStatus(sensorData, result.illuminance0);
216         updateSmokeStatus(sensorData, result.smoke0);
217         updateBatteryStatus(sensorData, result.devicepower0);
218         updateAddonStatus(status, result);
219         updated |= ShellyComponents.updateSensors(getThing(), status);
220         return updated;
221     }
222
223     private boolean updateRelayStatus(ShellySettingsStatus status, @Nullable Shelly2RelayStatus rs,
224             boolean channelUpdate) throws ShellyApiException {
225         if (rs == null) {
226             return false;
227         }
228         ShellyDeviceProfile profile = getProfile();
229
230         ShellySettingsRelay rstatus;
231         ShellyShortStatusRelay sr;
232         int rIdx = getRelayIdx(profile, rs.id);
233         if (profile.hasRelays) {
234             if (rIdx == -1) {
235                 throw new IllegalArgumentException("Update for invalid relay index");
236             }
237             rstatus = status.relays.get(rIdx);
238             sr = relayStatus.relays.get(rIdx);
239         } else {
240             rstatus = new ShellySettingsRelay();
241             sr = new ShellyShortStatusRelay();
242             rIdx = rs.id;
243         }
244
245         sr.isValid = rstatus.isValid = true;
246         sr.name = rstatus.name = status.name;
247         if (rs.output != null) {
248             sr.ison = rstatus.ison = getBool(rs.output);
249         }
250         if (getDouble(rs.timerStartetAt) > 0) {
251             int duration = (int) (now() - rs.timerStartetAt);
252             sr.timerRemaining = duration;
253         }
254         if (status.tmp == null) {
255             status.tmp = new ShellySensorTmp();
256         }
257         if (rs.temperature != null) {
258             if (status.tmp == null) {
259                 status.tmp = new ShellySensorTmp();
260             }
261             status.tmp.isValid = true;
262             status.tmp.tC = rs.temperature.tC;
263             status.tmp.tF = rs.temperature.tF;
264             status.tmp.units = "C";
265             sr.temperature = getDouble(rs.temperature.tC);
266             if (status.temperature == null || getDouble(rs.temperature.tC) > status.temperature) {
267                 status.temperature = sr.temperature;
268             }
269         }
270
271         if (rs.errors != null) {
272             for (String error : rs.errors) {
273                 sr.overpower = rstatus.overpower = SHELLY2_ERROR_OVERPOWER.equals(error);
274                 status.overload = SHELLY2_ERROR_OVERVOLTAGE.equals(error);
275                 status.overtemperature = SHELLY2_ERROR_OVERTEMP.equals(error);
276             }
277             sr.overtemperature = status.overtemperature;
278         }
279
280         ShellySettingsMeter sm = new ShellySettingsMeter();
281         ShellySettingsEMeter emeter = status.emeters != null ? status.emeters.get(rIdx) : new ShellySettingsEMeter();
282         if (rs.apower != null) {
283             sm.power = emeter.power = rs.apower;
284         }
285         if (rs.aenergy != null) {
286             // Gen2 reports Watt, needs to be converted to W/h
287             sm.total = emeter.total = rs.aenergy.total;
288             sm.counters = rs.aenergy.byMinute;
289             sm.timestamp = rs.aenergy.minuteTs;
290         }
291         if (rs.voltage != null) {
292             emeter.voltage = rs.voltage;
293         }
294         if (rs.current != null) {
295             emeter.current = rs.current;
296         }
297         if (rs.pf != null) {
298             emeter.pf = rs.pf;
299         }
300
301         if (profile.hasRelays) {
302             // Update internal structures
303             status.relays.set(rIdx, rstatus);
304             relayStatus.relays.set(rIdx, sr);
305         }
306
307         updateMeter(status, rIdx, sm, emeter, channelUpdate);
308         return channelUpdate && profile.hasRelays
309                 ? ShellyComponents.updateRelay((ShellyBaseHandler) getThing(), status, rIdx)
310                 : false;
311     }
312
313     private int getRelayIdx(ShellyDeviceProfile profile, @Nullable Integer id) {
314         if (id != null && profile.settings.relays != null) {
315             int idx = 0;
316             for (ShellySettingsRelay relay : profile.settings.relays) {
317                 if (relay.isValid && relay.id != null && relay.id.intValue() == id.intValue()) {
318                     return idx;
319                 }
320                 idx++;
321             }
322         }
323         return -1;
324     }
325
326     private void updateMeter(ShellySettingsStatus status, int id, ShellySettingsMeter sm, ShellySettingsEMeter emeter,
327             boolean channelUpdate) throws ShellyApiException {
328         if (getProfile().numMeters == 0) {
329             return;
330         }
331         sm.isValid = sm.power != null || sm.total != null;
332         emeter.isValid = emeter.current != null || emeter.voltage != null || emeter.power != null
333                 || emeter.total != null;
334         status.meters.set(id, sm);
335         status.emeters.set(id, emeter);
336         relayStatus.meters.set(id, sm);
337     }
338
339     private boolean updateEmStatus(ShellySettingsStatus status, @Nullable Shelly2StatusEm1 em, boolean channelUpdate)
340             throws ShellyApiException {
341         if (em == null) {
342             return false;
343         }
344
345         ShellySettingsMeter sm = new ShellySettingsMeter();
346         ShellySettingsEMeter emeter = status.emeters.get(em.id);
347         if (em.actPower != null) {
348             sm.power = emeter.power = em.actPower;
349         }
350         if (em.aptrPower != null) {
351             emeter.totalReturned = em.aptrPower;
352         }
353         if (em.voltage != null) {
354             emeter.voltage = em.voltage;
355         }
356         if (em.current != null) {
357             emeter.current = em.current;
358         }
359         if (em.pf != null) {
360             emeter.pf = em.pf;
361         }
362         // Update internal structures
363         updateMeter(status, em.id, sm, emeter, channelUpdate);
364
365         postAlarms(em.errors);
366         return channelUpdate ? ShellyComponents.updateMeters(getThing(), status) : false;
367     }
368
369     private boolean updateEmStatus(ShellySettingsStatus status, @Nullable Shelly2DeviceStatusEm em,
370             boolean channelUpdate) throws ShellyApiException {
371         if (em == null) {
372             return false;
373         }
374
375         if (em.totalCurrent != null) {
376             status.totalCurrent = em.totalCurrent;
377         }
378         if (em.totalActPower != null) {
379             status.totalPower = em.totalActPower;
380         }
381         if (em.totalAprtPower != null) {
382             status.totalReturned = em.totalAprtPower;
383         }
384
385         ShellySettingsMeter sm = new ShellySettingsMeter();
386         ShellySettingsEMeter emeter = status.emeters.get(0);
387         if (em.aActPower != null) {
388             sm.power = emeter.power = em.aActPower;
389         }
390         if (em.aAprtPower != null) {
391             emeter.totalReturned = em.aAprtPower;
392         }
393         if (em.aVoltage != null) {
394             emeter.voltage = em.aVoltage;
395         }
396         if (em.aCurrent != null) {
397             emeter.current = em.aCurrent;
398         }
399         if (em.aPF != null) {
400             emeter.pf = em.aPF;
401         }
402         // Update internal structures
403         updateMeter(status, 0, sm, emeter, channelUpdate);
404
405         if (status.emeters.size() > 1) {
406             sm = new ShellySettingsMeter();
407             emeter = status.emeters.get(1);
408             sm.isValid = emeter.isValid = true;
409             if (em.bActPower != null) {
410                 sm.power = emeter.power = em.bActPower;
411             }
412             if (em.bAprtPower != null) {
413                 emeter.totalReturned = em.bAprtPower;
414             }
415             if (em.bVoltage != null) {
416                 emeter.voltage = em.bVoltage;
417             }
418             if (em.bCurrent != null) {
419                 emeter.current = em.bCurrent;
420             }
421             if (em.bPF != null) {
422                 emeter.pf = em.bPF;
423             }
424             // Update internal structures
425             updateMeter(status, 1, sm, emeter, channelUpdate);
426         }
427
428         if (status.emeters.size() > 2) {
429             sm = new ShellySettingsMeter();
430             emeter = status.emeters.get(2);
431             sm.isValid = emeter.isValid = true;
432             if (em.cActPower != null) {
433                 sm.power = emeter.power = em.cActPower;
434             }
435             if (em.cAprtPower != null) {
436                 emeter.totalReturned = em.cAprtPower;
437             }
438             if (em.cVoltage != null) {
439                 emeter.voltage = em.cVoltage;
440             }
441             if (em.cCurrent != null) {
442                 emeter.current = em.cCurrent;
443             }
444             if (em.cPF != null) {
445                 emeter.pf = em.cPF;
446             }
447             // Update internal structures
448             updateMeter(status, 2, sm, emeter, channelUpdate);
449         }
450
451         return channelUpdate ? ShellyComponents.updateMeters(getThing(), status) : false;
452     }
453
454     protected @Nullable ArrayList<@Nullable ShellySettingsRoller> fillRollerSettings(ShellyDeviceProfile profile,
455             Shelly2GetConfigResult dc) {
456         if (dc.cover0 == null) {
457             return null;
458         }
459
460         ArrayList<@Nullable ShellySettingsRoller> rollers = new ArrayList<>();
461
462         addRollerSettings(rollers, dc.cover0);
463         fillRollerFavorites(profile, dc);
464         return rollers;
465     }
466
467     private void addRollerSettings(ArrayList<@Nullable ShellySettingsRoller> rollers,
468             @Nullable Shelly2DevConfigCover coverConfig) {
469         if (coverConfig == null) {
470             return;
471         }
472
473         ShellySettingsRoller settings = new ShellySettingsRoller();
474         settings.isValid = true;
475         settings.defaultState = coverConfig.initialState;
476         settings.inputMode = mapValue(MAP_INPUT_MODE, coverConfig.inMode);
477         settings.btnReverse = getBool(coverConfig.invertDirections) ? 1 : 0;
478         settings.swapInputs = coverConfig.swapInputs;
479         settings.maxtime = 0.0; // n/a
480         settings.maxtimeOpen = coverConfig.maxtimeOpen;
481         settings.maxtimeClose = coverConfig.maxtimeClose;
482         if (coverConfig.safetySwitch != null) {
483             settings.safetySwitch = coverConfig.safetySwitch.enable;
484             settings.safetyAction = coverConfig.safetySwitch.action;
485         }
486         if (coverConfig.obstructionDetection != null) {
487             settings.obstacleAction = coverConfig.obstructionDetection.action;
488             settings.obstacleDelay = coverConfig.obstructionDetection.holdoff.intValue();
489             settings.obstaclePower = coverConfig.obstructionDetection.powerThr;
490         }
491         rollers.add(settings);
492     }
493
494     private void fillRollerFavorites(ShellyDeviceProfile profile, Shelly2GetConfigResult dc) {
495         if (dc.sys.uiData.cover != null) {
496             String[] favorites = dc.sys.uiData.cover.split(",");
497             profile.settings.favorites = new ArrayList<>();
498             for (int i = 0; i < favorites.length; i++) {
499                 ShellyFavPos fav = new ShellyFavPos();
500                 fav.pos = Integer.parseInt(favorites[i]);
501                 fav.name = fav.pos + "%";
502                 profile.settings.favorites.add(fav);
503             }
504             profile.settings.favoritesEnabled = !profile.settings.favorites.isEmpty();
505             logger.debug("{}: Roller Favorites loaded: {}", thingName,
506                     profile.settings.favoritesEnabled ? profile.settings.favorites.size() : "none");
507         }
508     }
509
510     private boolean updateRollerStatus(ShellySettingsStatus status, @Nullable Shelly2CoverStatus cs,
511             boolean updateChannels) throws ShellyApiException {
512         if (cs == null) {
513             return false;
514         }
515
516         ShellyRollerStatus rs = status.rollers.get(cs.id);
517         ShellySettingsMeter sm = status.meters.get(cs.id);
518         ShellySettingsEMeter emeter = status.emeters.get(cs.id);
519         rs.isValid = sm.isValid = emeter.isValid = true;
520         if (cs.state != null) {
521             if (!getString(rs.state).equals(cs.state)) {
522                 logger.debug("{}: Roller status changed from {} to {}, updateChannels={}", thingName, rs.state,
523                         mapValue(MAP_ROLLER_STATE, cs.state), updateChannels);
524             }
525             rs.state = mapValue(MAP_ROLLER_STATE, cs.state);
526             rs.calibrating = SHELLY2_RSTATE_CALIB.equals(cs.state);
527         }
528         if (cs.currentPos != null) {
529             rs.currentPos = cs.currentPos;
530         }
531         if (cs.moveStartedAt != null) {
532             rs.duration = (int) (now() - cs.moveStartedAt.longValue());
533         }
534         if (cs.temperature != null && cs.temperature.tC > getDouble(status.temperature)) {
535             if (status.tmp == null) {
536                 status.tmp = new ShellySensorTmp();
537             }
538             status.temperature = status.tmp.tC = getDouble(cs.temperature.tC);
539         }
540         if (cs.apower != null) {
541             rs.power = sm.power = emeter.power = cs.apower;
542         }
543         if (cs.aenergy != null) {
544             sm.total = emeter.total = cs.aenergy.total;
545             sm.counters = cs.aenergy.byMinute;
546             if (cs.aenergy.minuteTs != null) {
547                 sm.timestamp = (long) cs.aenergy.minuteTs;
548             }
549         }
550         if (cs.voltage != null) {
551             emeter.voltage = cs.voltage;
552         }
553         if (cs.current != null) {
554             emeter.current = cs.current;
555         }
556         if (cs.pf != null) {
557             emeter.pf = cs.pf;
558         }
559
560         rollerStatus.set(cs.id, rs);
561         status.rollers.set(cs.id, rs);
562         relayStatus.meters.set(cs.id, sm);
563         status.meters.set(cs.id, sm);
564         status.emeters.set(cs.id, emeter);
565
566         postAlarms(cs.errors);
567         if (rs.calibrating != null && rs.calibrating) {
568             getThing().postEvent(SHELLY_EVENT_ROLLER_CALIB, false);
569         }
570
571         return updateChannels ? ShellyComponents.updateRoller((ShellyBaseHandler) getThing(), rs, cs.id) : false;
572     }
573
574     protected void fillDimmerSettings(ShellyDeviceProfile profile, Shelly2GetConfigResult dc) {
575         if (!profile.isDimmer || dc.light0 == null) {
576             return;
577         }
578
579         if (profile.settings.dimmers != null) {
580             ShellySettingsDimmer ds = profile.settings.dimmers.get(0);
581             ds.autoOn = dc.light0.autoOnDelay;
582             ds.autoOff = dc.light0.autoOffDelay;
583             ds.name = dc.light0.name;
584             profile.settings.dimmers.set(0, ds);
585         }
586     }
587
588     private boolean updateDimmerStatus(ShellySettingsStatus status, @Nullable Shelly2DeviceStatusLight value,
589             boolean channelUpdate) throws ShellyApiException {
590         ShellyDeviceProfile profile = getProfile();
591         if (!profile.isDimmer || value == null) {
592             return false;
593         }
594
595         ShellyShortLightStatus ds = status.dimmers.get(0);
596         if (value.brightness != null) {
597             ds.brightness = value.brightness.intValue();
598         }
599         ds.ison = value.output;
600         ds.hasTimer = value.timerStartedAt != null;
601         ds.timerDuration = getDuration(value.timerStartedAt, value.timerDuration);
602         status.dimmers.set(0, ds);
603         return channelUpdate ? ShellyComponents.updateDimmers(getThing(), status) : false;
604     }
605
606     protected @Nullable Integer getDuration(@Nullable Double timerStartedAt, @Nullable Integer timerDuration) {
607         if (timerStartedAt == null || timerDuration == null) {
608             return null;
609         }
610         int duration = (int) (now() - timerStartedAt.longValue());
611         return duration <= timerDuration ? timerDuration - duration : 0;
612     }
613
614     // Addon
615     private void updateAddonStatus(ShellySettingsStatus status, @Nullable Shelly2DeviceStatusResult ds)
616             throws ShellyApiException {
617         if (ds == null) {
618             return;
619         }
620
621         if (ds.temperature100 != null) {
622             if (status.extTemperature == null) {
623                 status.extTemperature = new ShellyExtTemperature();
624             }
625             status.extTemperature.sensor1 = updateExtTempSensor(ds.temperature100);
626             status.extTemperature.sensor2 = updateExtTempSensor(ds.temperature101);
627             status.extTemperature.sensor3 = updateExtTempSensor(ds.temperature102);
628             status.extTemperature.sensor4 = updateExtTempSensor(ds.temperature103);
629             status.extTemperature.sensor5 = updateExtTempSensor(ds.temperature104);
630         }
631         if (ds.humidity100 != null) {
632             status.extHumidity = new ShellyExtHumidity(ds.humidity100.rh);
633         }
634         if (ds.voltmeter100 != null) {
635             status.extVoltage = new ShellyExtVoltage(ds.voltmeter100.voltage);
636         }
637         if (ds.input100 != null) {
638             status.extDigitalInput = new ShellyExtDigitalInput(getBool(ds.input100.state));
639         }
640     }
641
642     private @Nullable ShellyShortTemp updateExtTempSensor(@Nullable Shelly2DeviceStatusTempId value) {
643         if (value != null) {
644             ShellyShortTemp temp = new ShellyShortTemp();
645             temp.hwID = value.id.toString();
646             temp.tC = value.tC;
647             temp.tF = value.tF;
648             return temp;
649         }
650         return null;
651     }
652
653     protected void updateHumidityStatus(ShellyStatusSensor sdata, @Nullable Shelly2DeviceStatusHumidity value) {
654         if (value == null) {
655             return;
656         }
657         if (sdata.hum == null) {
658             sdata.hum = new ShellySensorHum();
659         }
660         sdata.hum.value = getDouble(value.rh);
661     }
662
663     protected void updateTemperatureStatus(ShellyStatusSensor sdata, @Nullable Shelly2DeviceStatusTempId value) {
664         if (value == null) {
665             return;
666         }
667         if (sdata.tmp == null) {
668             sdata.tmp = new ShellySensorTmp();
669         }
670         sdata.tmp.isValid = true;
671         sdata.tmp.units = SHELLY_TEMP_CELSIUS;
672         sdata.tmp.tC = value.tC;
673         sdata.tmp.tF = value.tF;
674     }
675
676     protected void updateIlluminanceStatus(ShellyStatusSensor sdata, @Nullable Shelly2DeviceStatusIlluminance value) {
677         if (value == null) {
678             return;
679         }
680         if (sdata.lux == null) {
681             sdata.lux = new ShellySensorLux();
682         }
683         sdata.lux.isValid = value.lux != null;
684         sdata.lux.value = value.lux;
685         sdata.lux.illumination = value.illumination;
686     }
687
688     protected void updateSmokeStatus(ShellyStatusSensor sdata, @Nullable Shelly2DeviceStatusSmoke value) {
689         if (value == null) {
690             return;
691         }
692         sdata.smoke = getBool(value.alarm);
693         sdata.mute = getBool(value.mute);
694     }
695
696     protected void updateBatteryStatus(ShellyStatusSensor sdata, @Nullable Shelly2DeviceStatusPower value) {
697         if (value == null) {
698             return;
699         }
700         if (sdata.bat == null) {
701             sdata.bat = new ShellySensorBat();
702         }
703
704         if (value.battery != null) {
705             sdata.bat.voltage = value.battery.volt;
706             sdata.bat.value = value.battery.percent;
707         }
708         if (value.external != null) {
709             sdata.charger = value.external.present;
710         }
711     }
712
713     private void postAlarms(@Nullable ArrayList<@Nullable String> errors) throws ShellyApiException {
714         if (errors != null) {
715             for (String e : errors) {
716                 if (e != null) {
717                     getThing().postEvent(e, false);
718                 }
719             }
720         }
721     }
722
723     protected @Nullable ArrayList<ShellySettingsInput> fillInputSettings(ShellyDeviceProfile profile,
724             Shelly2GetConfigResult dc) {
725         if (dc.input0 == null) {
726             return null; // device has no input
727         }
728
729         ArrayList<ShellySettingsInput> inputs = new ArrayList<>();
730         addInputSettings(inputs, dc.input0);
731         addInputSettings(inputs, dc.input1);
732         addInputSettings(inputs, dc.input2);
733         addInputSettings(inputs, dc.input3);
734         return inputs;
735     }
736
737     private void addInputSettings(ArrayList<ShellySettingsInput> inputs, @Nullable Shelly2DevConfigInput ic) {
738         if (ic == null) {
739             return;
740         }
741
742         ShellySettingsInput settings = new ShellySettingsInput();
743         settings.btnType = getString(ic.type).equalsIgnoreCase(SHELLY2_INPUTT_BUTTON) ? SHELLY_BTNT_MOMENTARY
744                 : SHELLY_BTNT_EDGE;
745         inputs.add(settings);
746     }
747
748     protected boolean updateInputStatus(ShellySettingsStatus status, Shelly2DeviceStatusResult ds,
749             boolean updateChannels) throws ShellyApiException {
750         boolean updated = false;
751         updated |= addInputStatus(status, ds.input0, updateChannels);
752         updated |= addInputStatus(status, ds.input1, updateChannels);
753         updated |= addInputStatus(status, ds.input2, updateChannels);
754         updated |= addInputStatus(status, ds.input3, updateChannels);
755         status.inputs = relayStatus.inputs;
756         return updated;
757     }
758
759     private boolean addInputStatus(ShellySettingsStatus status, @Nullable Shelly2InputStatus is, boolean updateChannels)
760             throws ShellyApiException {
761         if (is == null) {
762             return false;
763         }
764         ShellyDeviceProfile profile = getProfile();
765
766         if (is.id == null || is.id > profile.numInputs) {
767             logger.debug("{}: Invalid input id: {}", thingName, is.id);
768             return false;
769         }
770
771         String group = profile.getInputGroup(is.id);
772         ShellyInputState input = relayStatus.inputs.size() > is.id ? relayStatus.inputs.get(is.id)
773                 : new ShellyInputState();
774         boolean updated = false;
775         input.input = getBool(is.state) ? 1 : 0; // old format Integer, new one Boolean
776         if (input.event == null && profile.inButtonMode(is.id)) {
777             input.event = "";
778             input.eventCount = 0;
779         }
780         if (is.percent != null) { // analogous input
781             status.extAnalogInput = new ShellyExtAnalogInput(getDouble(is.percent));
782         }
783         relayStatus.inputs.set(is.id, input);
784         if (updateChannels) {
785             updated |= updateChannel(group, CHANNEL_INPUT + profile.getInputSuffix(is.id), getOnOff(getBool(is.state)));
786         }
787         return updated;
788     }
789
790     protected Shelly2RpcBaseMessage buildRequest(String method, @Nullable Object params) throws ShellyApiException {
791         Shelly2RpcBaseMessage request = new Shelly2RpcBaseMessage();
792         request.id = Math.abs(random.nextInt());
793         request.src = thingName;
794         request.method = !method.contains(".") ? SHELLYRPC_METHOD_CLASS_SHELLY + "." + method : method;
795         request.params = params;
796         request.auth = authReq;
797         return request;
798     }
799
800     protected String mapValue(Map<String, String> map, @Nullable String key) {
801         String value;
802         boolean known = key != null && !key.isEmpty() && map.containsKey(key);
803         value = known ? getString(map.get(key)) : "";
804         logger.trace("{}: API value {} was mapped to {}", thingName, key, known ? value : "UNKNOWN");
805         return value;
806     }
807
808     protected boolean updateChannel(String group, String channel, State value) throws ShellyApiException {
809         return getThing().updateChannel(group, channel, value);
810     }
811
812     protected ShellyThingInterface getThing() throws ShellyApiException {
813         ShellyThingInterface t = thing;
814         if (t != null) {
815             return t;
816         }
817         throw new ShellyApiException("Thing/profile not initialized!");
818     }
819
820     protected ShellyDeviceProfile getProfile() throws ShellyApiException {
821         if (thing != null) {
822             return thing.getProfile();
823         }
824         throw new ShellyApiException("Unable to get profile, thing not initialized!");
825     }
826 }