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