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