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