]> git.basschouten.com Git - openhab-addons.git/blob
174817a999bb5eddba39b474e3457a9917e6fcd4
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2022 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
7  * This program and the accompanying materials are made available under the
8  * terms of the Eclipse Public License 2.0 which is available at
9  * http://www.eclipse.org/legal/epl-2.0
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.shelly.internal.handler;
14
15 import static org.openhab.binding.shelly.internal.ShellyBindingConstants.*;
16 import static org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.*;
17 import static org.openhab.binding.shelly.internal.util.ShellyUtils.*;
18
19 import org.eclipse.jdt.annotation.NonNullByDefault;
20 import org.eclipse.jetty.client.HttpClient;
21 import org.openhab.binding.shelly.internal.api.ShellyApiException;
22 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO;
23 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyRollerStatus;
24 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsDimmer;
25 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsRelay;
26 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsStatus;
27 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyShortLightStatus;
28 import org.openhab.binding.shelly.internal.api1.Shelly1CoapServer;
29 import org.openhab.binding.shelly.internal.config.ShellyBindingConfiguration;
30 import org.openhab.binding.shelly.internal.provider.ShellyChannelDefinitions;
31 import org.openhab.binding.shelly.internal.provider.ShellyTranslationProvider;
32 import org.openhab.core.library.types.DecimalType;
33 import org.openhab.core.library.types.IncreaseDecreaseType;
34 import org.openhab.core.library.types.OnOffType;
35 import org.openhab.core.library.types.PercentType;
36 import org.openhab.core.library.types.StopMoveType;
37 import org.openhab.core.library.types.UpDownType;
38 import org.openhab.core.library.unit.Units;
39 import org.openhab.core.thing.ChannelUID;
40 import org.openhab.core.thing.Thing;
41 import org.openhab.core.types.Command;
42 import org.slf4j.Logger;
43 import org.slf4j.LoggerFactory;
44
45 import com.google.gson.Gson;
46
47 /***
48  * The{@link ShellyRelayHandler} handles light (bulb+rgbw2) specific commands and status. All other commands will be
49  * handled by the generic thing handler.
50  *
51  * @author Markus Michels - Initial contribution
52  */
53 @NonNullByDefault
54 public class ShellyRelayHandler extends ShellyBaseHandler {
55     private final Logger logger = LoggerFactory.getLogger(ShellyRelayHandler.class);
56
57     /**
58      * Constructor
59      *
60      * @param thing The thing passed by the HandlerFactory
61      * @param bindingConfig configuration of the binding
62      * @param coapServer coap server instance
63      * @param localIP local IP of the openHAB host
64      * @param httpPort port of the openHAB HTTP API
65      */
66     public ShellyRelayHandler(final Thing thing, final ShellyTranslationProvider translationProvider,
67             final ShellyBindingConfiguration bindingConfig, final Shelly1CoapServer coapServer, final String localIP,
68             int httpPort, final HttpClient httpClient) {
69         super(thing, translationProvider, bindingConfig, coapServer, localIP, httpPort, httpClient);
70     }
71
72     @Override
73     public void initialize() {
74         super.initialize();
75     }
76
77     @Override
78     public boolean handleDeviceCommand(ChannelUID channelUID, Command command) throws ShellyApiException {
79         // Process command
80         String groupName = getString(channelUID.getGroupId());
81         Integer rIndex = 0;
82         if (groupName.startsWith(CHANNEL_GROUP_RELAY_CONTROL)
83                 && groupName.length() > CHANNEL_GROUP_RELAY_CONTROL.length()) {
84             rIndex = Integer.parseInt(substringAfter(channelUID.getGroupId(), CHANNEL_GROUP_RELAY_CONTROL)) - 1;
85         } else if (groupName.startsWith(CHANNEL_GROUP_ROL_CONTROL)
86                 && groupName.length() > CHANNEL_GROUP_ROL_CONTROL.length()) {
87             rIndex = Integer.parseInt(substringAfter(channelUID.getGroupId(), CHANNEL_GROUP_ROL_CONTROL)) - 1;
88         }
89
90         switch (channelUID.getIdWithoutGroup()) {
91             default:
92                 return false;
93
94             case CHANNEL_OUTPUT:
95                 if (!profile.isRoller) {
96                     // extract relay number of group name (relay0->0, relay1->1...)
97                     logger.debug("{}: Set relay output to {}", thingName, command);
98                     api.setRelayTurn(rIndex, command == OnOffType.ON ? SHELLY_API_ON : SHELLY_API_OFF);
99                 } else {
100                     logger.debug("{}: Device is in roller mode, channel command {} ignored", thingName, channelUID);
101                 }
102                 break;
103             case CHANNEL_BRIGHTNESS: // e.g.Dimmer, Duo
104                 handleBrightness(command, rIndex);
105                 break;
106
107             case CHANNEL_ROL_CONTROL_POS:
108             case CHANNEL_ROL_CONTROL_CONTROL:
109                 logger.debug("{}: Roller command/position {}", thingName, command);
110                 handleRoller(command, groupName, rIndex,
111                         channelUID.getIdWithoutGroup().equals(CHANNEL_ROL_CONTROL_CONTROL));
112
113                 // request updates the next 45sec to update roller position after it stopped
114                 requestUpdates(autoCoIoT ? 1 : 45 / UPDATE_STATUS_INTERVAL_SECONDS, false);
115                 break;
116
117             case CHANNEL_ROL_CONTROL_FAV:
118                 if (command instanceof Number) {
119                     int id = ((Number) command).intValue() - 1;
120                     int pos = profile.getRollerFav(id);
121                     if (pos > 0) {
122                         logger.debug("{}: Selecting favorite {}, position = {}", thingName, id, pos);
123                         api.setRollerPos(rIndex, pos);
124                         break;
125                     }
126                 }
127                 logger.debug("{}: Invalid favorite index: {}", thingName, command);
128                 break;
129
130             case CHANNEL_TIMER_AUTOON:
131                 logger.debug("{}: Set Auto-ON timer to {}", thingName, command);
132                 api.setTimer(rIndex, SHELLY_TIMER_AUTOON, getNumber(command).intValue());
133                 break;
134             case CHANNEL_TIMER_AUTOOFF:
135                 logger.debug("{}: Set Auto-OFF timer to {}", thingName, command);
136                 api.setTimer(rIndex, SHELLY_TIMER_AUTOOFF, getNumber(command).intValue());
137                 break;
138         }
139         return true;
140     }
141
142     /**
143      * Brightness channel has 2 functions: Switch On/Off (OnOnType) and setting brightness (PercentType)
144      * There is some more logic in the control. When brightness is set to 0 the control sends also an OFF command
145      * When current brightness is 0 and slider will be moved the new brightness will be set, but also a ON command is
146      * send.
147      *
148      * @param command
149      * @param index
150      * @throws ShellyApiException
151      */
152     private void handleBrightness(Command command, Integer index) throws ShellyApiException {
153         Integer value = -1;
154         if (command instanceof PercentType) { // Dimmer
155             value = ((PercentType) command).intValue();
156         } else if (command instanceof DecimalType) { // Number
157             value = ((DecimalType) command).intValue();
158         } else if (command instanceof OnOffType) { // Switch
159             logger.debug("{}: Switch output {}", thingName, command);
160             updateBrightnessChannel(index, (OnOffType) command, value);
161             return;
162         } else if (command instanceof IncreaseDecreaseType) {
163             ShellyShortLightStatus light = api.getLightStatus(index);
164             if (command == IncreaseDecreaseType.INCREASE) {
165                 value = Math.min(light.brightness + DIM_STEPSIZE, 100);
166             } else {
167                 value = Math.max(light.brightness - DIM_STEPSIZE, 0);
168             }
169             logger.debug("{}: Increase/Decrease brightness from {} to {}", thingName, light.brightness, value);
170         }
171         validateRange("brightness", value, 0, 100);
172
173         // Switch light off on brightness = 0
174         if (value == 0) {
175             logger.debug("{}: Brightness=0 -> switch output OFF", thingName);
176             updateBrightnessChannel(index, OnOffType.OFF, 0);
177         } else {
178             logger.debug("{}: Setting dimmer brightness to {}", thingName, value);
179             updateBrightnessChannel(index, OnOffType.ON, value);
180         }
181     }
182
183     private void updateBrightnessChannel(int lightId, OnOffType power, int brightness) throws ShellyApiException {
184         updateChannel(CHANNEL_COLOR_WHITE, CHANNEL_BRIGHTNESS + "$Switch", power);
185         if (brightness > 0) {
186             api.setBrightness(lightId, brightness, config.brightnessAutoOn);
187         } else {
188             api.setRelayTurn(lightId, power == OnOffType.ON ? SHELLY_API_ON : SHELLY_API_OFF);
189             if (brightness >= 0) { // ignore -1
190                 updateChannel(CHANNEL_COLOR_WHITE, CHANNEL_BRIGHTNESS + "$Value",
191                         toQuantityType((double) (power == OnOffType.ON ? brightness : 0), DIGITS_NONE, Units.PERCENT));
192             }
193         }
194     }
195
196     @Override
197     public boolean updateDeviceStatus(ShellySettingsStatus status) throws ShellyApiException {
198         // map status to channels
199         boolean updated = false;
200         updated |= updateRelays(status);
201         updated |= updateDimmers(status);
202         updated |= updateLed(status);
203         return updated;
204     }
205
206     /**
207      * Handle Roller Commands
208      *
209      * @param command from handleCommand()
210      * @param groupName relay, roller...
211      * @param index relay number
212      * @param isControl true: is the Rollershutter channel, false: rollerpos channel
213      * @throws ShellyApiException
214      */
215     private void handleRoller(Command command, String groupName, Integer index, boolean isControl)
216             throws ShellyApiException {
217         int position = -1;
218
219         if ((command instanceof UpDownType) || (command instanceof OnOffType)) {
220             ShellyRollerStatus rstatus = api.getRollerStatus(index);
221
222             if (!getString(rstatus.state).isEmpty() && !getString(rstatus.state).equals(SHELLY_ALWD_ROLLER_TURN_STOP)) {
223                 if ((command == UpDownType.UP && getString(rstatus.state).equals(SHELLY_ALWD_ROLLER_TURN_OPEN))
224                         || (command == UpDownType.DOWN
225                                 && getString(rstatus.state).equals(SHELLY_ALWD_ROLLER_TURN_CLOSE))) {
226                     logger.debug("{}: Roller is already in requested position ({}), ignore command {}", thingName,
227                             getString(rstatus.state), command);
228                     requestUpdates(1, false);
229                     return;
230                 }
231             }
232
233             if (command == UpDownType.UP || command == OnOffType.ON
234                     || ((command instanceof DecimalType) && (((DecimalType) command).intValue() == 100))) {
235                 logger.debug("{}: Open roller", thingName);
236                 int shpos = profile.getRollerFav(config.favoriteUP - 1);
237                 if (shpos > 0) {
238                     logger.debug("{}: Use favoriteUP id {} for positioning roller({}%)", thingName, config.favoriteUP,
239                             shpos);
240                     api.setRollerPos(index, shpos);
241                     position = shpos;
242                 } else {
243                     api.setRollerTurn(index, SHELLY_ALWD_ROLLER_TURN_OPEN);
244                     position = SHELLY_MIN_ROLLER_POS;
245                 }
246             } else if (command == UpDownType.DOWN || command == OnOffType.OFF
247                     || ((command instanceof DecimalType) && (((DecimalType) command).intValue() == 0))) {
248                 logger.debug("{}: Closing roller", thingName);
249                 int shpos = profile.getRollerFav(config.favoriteDOWN - 1);
250                 if (shpos > 0) {
251                     // use favorite position
252                     logger.debug("{}: Use favoriteDOWN id {} for positioning roller ({}%)", thingName,
253                             config.favoriteDOWN, shpos);
254                     api.setRollerPos(index, shpos);
255                     position = shpos;
256                 } else {
257                     api.setRollerTurn(index, SHELLY_ALWD_ROLLER_TURN_CLOSE);
258                     position = SHELLY_MAX_ROLLER_POS;
259                 }
260             }
261         } else if (command == StopMoveType.STOP) {
262             logger.debug("{}: Stop roller", thingName);
263             api.setRollerTurn(index, SHELLY_ALWD_ROLLER_TURN_STOP);
264         } else {
265             logger.debug("{}: Set roller to position {}", thingName, command);
266             if (command instanceof PercentType) {
267                 PercentType p = (PercentType) command;
268                 position = p.intValue();
269             } else if (command instanceof DecimalType) {
270                 DecimalType d = (DecimalType) command;
271                 position = d.intValue();
272             } else {
273                 throw new IllegalArgumentException(
274                         "Invalid value type for roller control/position" + command.getClass().toString());
275             }
276
277             // take position from RollerShutter control and map to Shelly positon
278             // OH: 0=closed, 100=open; Shelly 0=open, 100=closed)
279             // take position 1:1 from position channel
280             position = isControl ? SHELLY_MAX_ROLLER_POS - position : position;
281             validateRange("roller position", position, SHELLY_MIN_ROLLER_POS, SHELLY_MAX_ROLLER_POS);
282
283             logger.debug("{}: Changing roller position to {}", thingName, position);
284             api.setRollerPos(index, position);
285         }
286
287         if (position != -1) {
288             // make sure both are in sync
289             if (isControl) {
290                 int pos = SHELLY_MAX_ROLLER_POS - Math.max(0, Math.min(position, SHELLY_MAX_ROLLER_POS));
291                 logger.debug("{}: Set roller position for control channel to {}", thingName, pos);
292                 updateChannel(groupName, CHANNEL_ROL_CONTROL_CONTROL, new PercentType(pos));
293             } else {
294                 logger.debug("{}: Set roller position channel to {}", thingName, position);
295                 updateChannel(groupName, CHANNEL_ROL_CONTROL_POS, new PercentType(position));
296             }
297         }
298     }
299
300     /**
301      * Auto-create relay channels depending on relay type/mode
302      */
303     private void createRelayChannels(ShellySettingsRelay relay, int idx) {
304         if (!areChannelsCreated()) {
305             updateChannelDefinitions(ShellyChannelDefinitions.createRelayChannels(getThing(), profile, relay, idx));
306         }
307     }
308
309     private void createDimmerChannels(ShellySettingsStatus dstatus, int idx) {
310         if (!areChannelsCreated()) {
311             updateChannelDefinitions(ShellyChannelDefinitions.createDimmerChannels(getThing(), profile, dstatus, idx));
312         }
313     }
314
315     private void createRollerChannels(ShellyRollerStatus roller) {
316         if (!areChannelsCreated()) {
317             updateChannelDefinitions(ShellyChannelDefinitions.createRollerChannels(getThing(), roller));
318         }
319     }
320
321     /**
322      * Update Relay/Roller channels
323      *
324      * @param th Thing Handler instance
325      * @param profile ShellyDeviceProfile
326      * @param status Last ShellySettingsStatus
327      *
328      * @throws ShellyApiException
329      */
330     public boolean updateRelays(ShellySettingsStatus status) throws ShellyApiException {
331         boolean updated = false;
332         // Check for Relay in Standard Mode
333         if (profile.hasRelays && !profile.isDimmer) {
334             double voltage = -1;
335             if (status.voltage == null && profile.settings.supplyVoltage != null) {
336                 // Shelly 1PM/1L (fix)
337                 voltage = profile.settings.supplyVoltage == 0 ? 110.0 : 220.0;
338             } else {
339                 // Shelly 2.5 (measured)
340                 voltage = getDouble(status.voltage);
341             }
342             if (voltage > 0) {
343                 updated |= updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_VOLTAGE,
344                         toQuantityType(voltage, DIGITS_VOLT, Units.VOLT));
345             }
346         }
347
348         if (profile.hasRelays && !profile.isRoller) {
349             logger.trace("{}: Updating {} relay(s)", thingName, profile.numRelays);
350             for (int i = 0; i < status.relays.size(); i++) {
351                 createRelayChannels(status.relays.get(i), i);
352                 updated |= ShellyComponents.updateRelay(this, status, i);
353                 i++;
354             }
355         } else {
356             // Check for Relay in Roller Mode
357             logger.trace("{}: Updating {} rollers", thingName, profile.numRollers);
358             for (int i = 0; i < profile.numRollers; i++) {
359                 ShellyRollerStatus roller = status.rollers.get(i);
360                 createRollerChannels(roller);
361                 updated |= ShellyComponents.updateRoller(this, roller, i);
362             }
363         }
364
365         return updated;
366     }
367
368     /**
369      * Update Relay/Roller channels
370      *
371      * @param th Thing Handler instance
372      * @param profile ShellyDeviceProfile
373      * @param status Last ShellySettingsStatus
374      *
375      * @throws ShellyApiException
376      */
377     public boolean updateDimmers(ShellySettingsStatus orgStatus) throws ShellyApiException {
378         boolean updated = false;
379         if (profile.isDimmer) {
380             // We need to fixup the returned Json: The dimmer returns light[] element, which is ok, but it doesn't have
381             // the same structure as lights[] from Bulb,RGBW2 and Duo. The tag gets replaced by dimmers[] so that Gson
382             // maps to a different structure (ShellyShortLight).
383             Gson gson = new Gson();
384             ShellySettingsStatus dstatus = fromJson(gson, Shelly1ApiJsonDTO.fixDimmerJson(orgStatus.json),
385                     ShellySettingsStatus.class);
386
387             logger.trace("{}: Updating {} dimmers(s)", thingName, dstatus.dimmers.size());
388             int l = 0;
389             for (ShellyShortLightStatus dimmer : dstatus.dimmers) {
390                 Integer r = l + 1;
391                 String groupName = profile.numRelays <= 1 ? CHANNEL_GROUP_DIMMER_CONTROL
392                         : CHANNEL_GROUP_DIMMER_CONTROL + r.toString();
393
394                 createDimmerChannels(dstatus, l);
395
396                 // On a status update we map a dimmer.ison = false to brightness 0 rather than the device's brightness
397                 // and send a OFF status to the same channel.
398                 // When the device's brightness is > 0 we send the new value to the channel and a ON command
399                 if (dimmer.ison) {
400                     updated |= updateChannel(groupName, CHANNEL_BRIGHTNESS + "$Switch", OnOffType.ON);
401                     updated |= updateChannel(groupName, CHANNEL_BRIGHTNESS + "$Value",
402                             toQuantityType((double) getInteger(dimmer.brightness), DIGITS_NONE, Units.PERCENT));
403                 } else {
404                     updated |= updateChannel(groupName, CHANNEL_BRIGHTNESS + "$Switch", OnOffType.OFF);
405                     updated |= updateChannel(groupName, CHANNEL_BRIGHTNESS + "$Value",
406                             toQuantityType(0.0, DIGITS_NONE, Units.PERCENT));
407                 }
408
409                 if (profile.settings.dimmers != null) {
410                     ShellySettingsDimmer dsettings = profile.settings.dimmers.get(l);
411                     if (dsettings != null) {
412                         updated |= updateChannel(groupName, CHANNEL_TIMER_AUTOON,
413                                 toQuantityType(getDouble(dsettings.autoOn), Units.SECOND));
414                         updated |= updateChannel(groupName, CHANNEL_TIMER_AUTOOFF,
415                                 toQuantityType(getDouble(dsettings.autoOff), Units.SECOND));
416                     }
417                 }
418
419                 l++;
420             }
421         }
422         return updated;
423     }
424
425     /**
426      * Update LED channels
427      *
428      * @param th Thing Handler instance
429      * @param profile ShellyDeviceProfile
430      * @param status Last ShellySettingsStatus
431      */
432     public boolean updateLed(ShellySettingsStatus status) {
433         boolean updated = false;
434         updated |= updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_LED_STATUS_DISABLE,
435                 getOnOff(profile.settings.ledStatusDisable));
436         updated |= updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_LED_POWER_DISABLE,
437                 getOnOff(profile.settings.ledPowerDisable));
438         return updated;
439     }
440 }