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