]> git.basschouten.com Git - openhab-addons.git/blob
70f052bb6566f49399208996cd9195b06f979f54
[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.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.ShellyRollerStatus;
23 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsRelay;
24 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsStatus;
25 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyShortLightStatus;
26 import org.openhab.binding.shelly.internal.api1.Shelly1CoapServer;
27 import org.openhab.binding.shelly.internal.config.ShellyBindingConfiguration;
28 import org.openhab.binding.shelly.internal.provider.ShellyChannelDefinitions;
29 import org.openhab.binding.shelly.internal.provider.ShellyTranslationProvider;
30 import org.openhab.core.library.types.DecimalType;
31 import org.openhab.core.library.types.IncreaseDecreaseType;
32 import org.openhab.core.library.types.OnOffType;
33 import org.openhab.core.library.types.PercentType;
34 import org.openhab.core.library.types.StopMoveType;
35 import org.openhab.core.library.types.UpDownType;
36 import org.openhab.core.library.unit.Units;
37 import org.openhab.core.thing.ChannelUID;
38 import org.openhab.core.thing.Thing;
39 import org.openhab.core.types.Command;
40 import org.slf4j.Logger;
41 import org.slf4j.LoggerFactory;
42
43 /***
44  * The{@link ShellyRelayHandler} handles light (bulb+rgbw2) specific commands and status. All other commands will be
45  * handled by the generic thing handler.
46  *
47  * @author Markus Michels - Initial contribution
48  */
49 @NonNullByDefault
50 public class ShellyRelayHandler extends ShellyBaseHandler {
51     private final Logger logger = LoggerFactory.getLogger(ShellyRelayHandler.class);
52
53     /**
54      * Constructor
55      *
56      * @param thing The thing passed by the HandlerFactory
57      * @param bindingConfig configuration of the binding
58      * @param coapServer coap server instance
59      * @param localIP local IP of the openHAB host
60      * @param httpPort port of the openHAB HTTP API
61      */
62     public ShellyRelayHandler(final Thing thing, final ShellyTranslationProvider translationProvider,
63             final ShellyBindingConfiguration bindingConfig, ShellyThingTable thingTable,
64             final Shelly1CoapServer coapServer, final HttpClient httpClient) {
65         super(thing, translationProvider, bindingConfig, thingTable, coapServer, httpClient);
66     }
67
68     @Override
69     public void initialize() {
70         super.initialize();
71     }
72
73     @Override
74     public boolean handleDeviceCommand(ChannelUID channelUID, Command command) throws ShellyApiException {
75         // Process command
76         String groupName = getString(channelUID.getGroupId());
77         Integer rIndex = 0;
78         if (groupName.startsWith(CHANNEL_GROUP_RELAY_CONTROL)
79                 && groupName.length() > CHANNEL_GROUP_RELAY_CONTROL.length()) {
80             rIndex = Integer.parseInt(substringAfter(channelUID.getGroupId(), CHANNEL_GROUP_RELAY_CONTROL)) - 1;
81         } else if (groupName.startsWith(CHANNEL_GROUP_ROL_CONTROL)
82                 && groupName.length() > CHANNEL_GROUP_ROL_CONTROL.length()) {
83             rIndex = Integer.parseInt(substringAfter(channelUID.getGroupId(), CHANNEL_GROUP_ROL_CONTROL)) - 1;
84         }
85
86         switch (channelUID.getIdWithoutGroup()) {
87             default:
88                 return false;
89
90             case CHANNEL_OUTPUT:
91                 if (!profile.isRoller) {
92                     // extract relay number of group name (relay0->0, relay1->1...)
93                     logger.debug("{}: Set relay output to {}", thingName, command);
94                     api.setRelayTurn(rIndex, command == OnOffType.ON ? SHELLY_API_ON : SHELLY_API_OFF);
95                 } else {
96                     logger.debug("{}: Device is in roller mode, channel command {} ignored", thingName, channelUID);
97                 }
98                 break;
99             case CHANNEL_BRIGHTNESS: // e.g.Dimmer, Duo
100                 handleBrightness(command, rIndex);
101                 break;
102
103             case CHANNEL_ROL_CONTROL_POS:
104             case CHANNEL_ROL_CONTROL_CONTROL:
105                 logger.debug("{}: Roller command/position {}", thingName, command);
106                 handleRoller(command, groupName, rIndex,
107                         channelUID.getIdWithoutGroup().equals(CHANNEL_ROL_CONTROL_CONTROL));
108
109                 // request updates the next 45sec to update roller position after it stopped
110                 if (!autoCoIoT && !profile.isGen2) {
111                     requestUpdates(45 / UPDATE_STATUS_INTERVAL_SECONDS, false);
112                 }
113                 break;
114
115             case CHANNEL_ROL_CONTROL_FAV:
116                 if (command instanceof Number) {
117                     int id = ((Number) command).intValue() - 1;
118                     int pos = profile.getRollerFav(id);
119                     if (pos > 0) {
120                         logger.debug("{}: Selecting favorite {}, position = {}", thingName, id, pos);
121                         api.setRollerPos(rIndex, pos);
122                         break;
123                     }
124                 }
125                 logger.debug("{}: Invalid favorite index: {}", thingName, command);
126                 break;
127
128             case CHANNEL_TIMER_AUTOON:
129                 logger.debug("{}: Set Auto-ON timer to {}", thingName, command);
130                 api.setAutoTimer(rIndex, SHELLY_TIMER_AUTOON, getNumber(command).doubleValue());
131                 break;
132             case CHANNEL_TIMER_AUTOOFF:
133                 logger.debug("{}: Set Auto-OFF timer to {}", thingName, command);
134                 api.setAutoTimer(rIndex, SHELLY_TIMER_AUTOOFF, getNumber(command).doubleValue());
135                 break;
136             case CHANNEL_DEVST_RESETTOTAL:
137                 logger.debug("{}: Reset Meter Totals", thingName);
138                 api.resetMeterTotal(0); // currently there is only 1 emdata component
139                 updateChannel(groupName, CHANNEL_DEVST_RESETTOTAL, OnOffType.OFF);
140                 break;
141         }
142         return true;
143     }
144
145     /**
146      * Brightness channel has 2 functions: Switch On/Off (OnOnType) and setting brightness (PercentType)
147      * There is some more logic in the control. When brightness is set to 0 the control sends also an OFF command
148      * When current brightness is 0 and slider will be moved the new brightness will be set, but also an ON command is
149      * sent.
150      *
151      * @param command
152      * @param index
153      * @throws ShellyApiException
154      */
155     private void handleBrightness(Command command, Integer index) throws ShellyApiException {
156         Integer value = -1;
157         if (command instanceof PercentType) { // Dimmer
158             value = ((PercentType) command).intValue();
159         } else if (command instanceof DecimalType) { // Number
160             value = ((DecimalType) command).intValue();
161         } else if (command instanceof OnOffType) { // Switch
162             logger.debug("{}: Switch output {}", thingName, command);
163             updateBrightnessChannel(index, (OnOffType) command, value);
164             return;
165         } else if (command instanceof IncreaseDecreaseType) {
166             ShellyShortLightStatus light = api.getLightStatus(index);
167             if (command == IncreaseDecreaseType.INCREASE) {
168                 value = Math.min(light.brightness + DIM_STEPSIZE, 100);
169             } else {
170                 value = Math.max(light.brightness - DIM_STEPSIZE, 0);
171             }
172             logger.debug("{}: Increase/Decrease brightness from {} to {}", thingName, light.brightness, value);
173         }
174         validateRange("brightness", value, 0, 100);
175
176         // Switch light off on brightness = 0
177         if (value == 0) {
178             logger.debug("{}: Brightness=0 -> switch output OFF", thingName);
179             updateBrightnessChannel(index, OnOffType.OFF, 0);
180         } else {
181             logger.debug("{}: Setting dimmer brightness to {}", thingName, value);
182             updateBrightnessChannel(index, OnOffType.ON, value);
183         }
184     }
185
186     private void updateBrightnessChannel(int lightId, OnOffType power, int brightness) throws ShellyApiException {
187         updateChannel(CHANNEL_COLOR_WHITE, CHANNEL_BRIGHTNESS + "$Switch", power);
188         if (brightness > 0) {
189             api.setBrightness(lightId, brightness, config.brightnessAutoOn);
190         } else {
191             api.setLightTurn(lightId, power == OnOffType.ON ? SHELLY_API_ON : SHELLY_API_OFF);
192             if (brightness >= 0) { // ignore -1
193                 updateChannel(CHANNEL_COLOR_WHITE, CHANNEL_BRIGHTNESS + "$Value",
194                         toQuantityType((double) (power == OnOffType.ON ? brightness : 0), DIGITS_NONE, Units.PERCENT));
195             }
196         }
197     }
198
199     @Override
200     public boolean updateDeviceStatus(ShellySettingsStatus status) throws ShellyApiException {
201         // map status to channels
202         boolean updated = false;
203         updated |= updateRelays(status);
204         updated |= ShellyComponents.updateDimmers(this, status);
205         updated |= updateLed(status);
206         return updated;
207     }
208
209     /**
210      * Handle Roller Commands
211      *
212      * @param command from handleCommand()
213      * @param groupName relay, roller...
214      * @param index relay number
215      * @param isControl true: is the Rollershutter channel, false: rollerpos channel
216      * @throws ShellyApiException
217      */
218     private void handleRoller(Command command, String groupName, Integer index, boolean isControl)
219             throws ShellyApiException {
220         int position = -1;
221
222         if ((command instanceof UpDownType) || (command instanceof OnOffType)) {
223             ShellyRollerStatus rstatus = api.getRollerStatus(index);
224
225             if (!getString(rstatus.state).isEmpty() && !getString(rstatus.state).equals(SHELLY_ALWD_ROLLER_TURN_STOP)) {
226                 if ((command == UpDownType.UP && getString(rstatus.state).equals(SHELLY_ALWD_ROLLER_TURN_OPEN))
227                         || (command == UpDownType.DOWN
228                                 && getString(rstatus.state).equals(SHELLY_ALWD_ROLLER_TURN_CLOSE))) {
229                     logger.debug("{}: Roller is already in requested position ({}), ignore command {}", thingName,
230                             getString(rstatus.state), command);
231                     requestUpdates(1, false);
232                     return;
233                 }
234             }
235
236             if (command == UpDownType.UP || command == OnOffType.ON
237                     || ((command instanceof DecimalType) && (((DecimalType) command).intValue() == 100))) {
238                 logger.debug("{}: Open roller", thingName);
239                 int shpos = profile.getRollerFav(config.favoriteUP - 1);
240                 if (shpos > 0) {
241                     logger.debug("{}: Use favoriteUP id {} for positioning roller({}%)", thingName, config.favoriteUP,
242                             shpos);
243                     api.setRollerPos(index, shpos);
244                     position = shpos;
245                 } else {
246                     api.setRollerTurn(index, SHELLY_ALWD_ROLLER_TURN_OPEN);
247                 }
248             } else if (command == UpDownType.DOWN || command == OnOffType.OFF
249                     || ((command instanceof DecimalType) && (((DecimalType) command).intValue() == 0))) {
250                 logger.debug("{}: Closing roller", thingName);
251                 int shpos = profile.getRollerFav(config.favoriteDOWN - 1);
252                 if (shpos > 0) {
253                     // use favorite position
254                     logger.debug("{}: Use favoriteDOWN id {} for positioning roller ({}%)", thingName,
255                             config.favoriteDOWN, shpos);
256                     api.setRollerPos(index, shpos);
257                     position = shpos;
258                 } else {
259                     api.setRollerTurn(index, SHELLY_ALWD_ROLLER_TURN_CLOSE);
260                 }
261             }
262         } else if (command == StopMoveType.STOP) {
263             logger.debug("{}: Stop roller", thingName);
264             api.setRollerTurn(index, SHELLY_ALWD_ROLLER_TURN_STOP);
265         } else {
266             logger.debug("{}: Set roller to position {}", thingName, command);
267             if (command instanceof PercentType) {
268                 PercentType p = (PercentType) command;
269                 position = p.intValue();
270             } else if (command instanceof DecimalType) {
271                 DecimalType d = (DecimalType) command;
272                 position = d.intValue();
273             } else {
274                 throw new IllegalArgumentException(
275                         "Invalid value type for roller control/position" + command.getClass().toString());
276             }
277
278             // take position from RollerShutter control and map to Shelly positon
279             // OH: 0=closed, 100=open; Shelly 0=open, 100=closed)
280             // take position 1:1 from position channel
281             position = isControl ? SHELLY_MAX_ROLLER_POS - position : position;
282             validateRange("roller position", position, SHELLY_MIN_ROLLER_POS, SHELLY_MAX_ROLLER_POS);
283
284             logger.debug("{}: Changing roller position to {}", thingName, position);
285             api.setRollerPos(index, position);
286         }
287     }
288
289     /**
290      * Auto-create relay channels depending on relay type/mode
291      */
292     private void createRelayChannels(ShellySettingsRelay relay, int idx) {
293         if (!areChannelsCreated()) {
294             updateChannelDefinitions(ShellyChannelDefinitions.createRelayChannels(getThing(), profile, relay, idx));
295         }
296     }
297
298     private void createRollerChannels(ShellyRollerStatus roller) {
299         if (!areChannelsCreated()) {
300             updateChannelDefinitions(ShellyChannelDefinitions.createRollerChannels(getThing(), roller));
301         }
302     }
303
304     /**
305      * Update Relay/Roller channels
306      *
307      * @param th Thing Handler instance
308      * @param profile ShellyDeviceProfile
309      * @param status Last ShellySettingsStatus
310      *
311      * @throws ShellyApiException
312      */
313     public boolean updateRelays(ShellySettingsStatus status) throws ShellyApiException {
314         boolean updated = false;
315         if (profile.hasRelays && !profile.isDimmer) {
316             double voltage = -1;
317             if (status.voltage == null && profile.settings.supplyVoltage != null) {
318                 // Shelly 1PM/1L (fix)
319                 voltage = profile.settings.supplyVoltage == 0 ? 110.0 : 220.0;
320             } else {
321                 // Shelly 2.5 (measured)
322                 voltage = getDouble(status.voltage);
323             }
324             if (voltage > 0) {
325                 updated |= updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_VOLTAGE,
326                         toQuantityType(voltage, DIGITS_VOLT, Units.VOLT));
327             }
328
329             if (!profile.isRoller) {
330                 logger.trace("{}: Updating {} relay(s)", thingName, profile.numRelays);
331                 for (int i = 0; i < status.relays.size(); i++) {
332                     createRelayChannels(status.relays.get(i), i);
333                     updated |= ShellyComponents.updateRelay(this, status, i);
334                 }
335             } else {
336                 // Check for Relay in Roller Mode
337                 logger.trace("{}: Updating {} rollers", thingName, profile.numRollers);
338                 for (int i = 0; i < profile.numRollers; i++) {
339                     ShellyRollerStatus roller = status.rollers.get(i);
340                     createRollerChannels(roller);
341                     updated |= ShellyComponents.updateRoller(this, roller, i);
342                 }
343             }
344         }
345         return updated;
346     }
347
348     /**
349      * Update LED channels
350      *
351      * @param th Thing Handler instance
352      * @param profile ShellyDeviceProfile
353      * @param status Last ShellySettingsStatus
354      */
355     public boolean updateLed(ShellySettingsStatus status) {
356         boolean updated = false;
357         updated |= updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_LED_STATUS_DISABLE,
358                 getOnOff(profile.settings.ledStatusDisable));
359         updated |= updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_LED_POWER_DISABLE,
360                 getOnOff(profile.settings.ledPowerDisable));
361         return updated;
362     }
363 }