]> git.basschouten.com Git - openhab-addons.git/blob
152b48daf9bf1e0f9788f55d4b51d0143ef0926e
[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      * Brightness channel has 2 functions: Switch On/Off (OnOnType) and setting brightness (PercentType)
135      * There is some more logic in the control. When brightness is set to 0 the control sends also an OFF command
136      * When current brightness is 0 and slider will be moved the new brightness will be set, but also a ON command is
137      * send.
138      *
139      * @param command
140      * @param index
141      * @throws ShellyApiException
142      */
143     private void handleBrightness(Command command, Integer index) throws ShellyApiException {
144         Integer value = -1;
145         if (command instanceof PercentType) { // Dimmer
146             value = ((PercentType) command).intValue();
147         } else if (command instanceof DecimalType) { // Number
148             value = ((DecimalType) command).intValue();
149         } else if (command instanceof OnOffType) { // Switch
150             logger.debug("{}: Switch output {}", thingName, command);
151             updateBrightnessChannel(index, (OnOffType) command, value);
152             return;
153         } else if (command instanceof IncreaseDecreaseType) {
154             ShellyShortLightStatus light = api.getLightStatus(index);
155             if (((IncreaseDecreaseType) command).equals(IncreaseDecreaseType.INCREASE)) {
156                 value = Math.min(light.brightness + DIM_STEPSIZE, 100);
157             } else {
158                 value = Math.max(light.brightness - DIM_STEPSIZE, 0);
159             }
160             logger.debug("{}: Increase/Decrease brightness from {} to {}", thingName, light.brightness, value);
161         }
162         validateRange("brightness", value, 0, 100);
163
164         // Switch light off on brightness = 0
165         if (value == 0) {
166             logger.debug("{}: Brightness=0 -> switch output OFF", thingName);
167             updateBrightnessChannel(index, OnOffType.OFF, 0);
168         } else {
169             logger.debug("{}: Setting dimmer brightness to {}", thingName, value);
170             updateBrightnessChannel(index, OnOffType.ON, value);
171         }
172     }
173
174     private void updateBrightnessChannel(int lightId, OnOffType power, int brightness) throws ShellyApiException {
175         if (brightness > 0) {
176             api.setBrightness(lightId, brightness, config.brightnessAutoOn);
177         } else {
178             api.setRelayTurn(lightId, power == OnOffType.ON ? SHELLY_API_ON : SHELLY_API_OFF);
179         }
180         updateChannel(CHANNEL_COLOR_WHITE, CHANNEL_BRIGHTNESS + "$Switch", power);
181         updateChannel(CHANNEL_COLOR_WHITE, CHANNEL_BRIGHTNESS + "$Value",
182                 toQuantityType(new Double(power == OnOffType.ON ? brightness : 0), DIGITS_NONE, Units.PERCENT));
183     }
184
185     @Override
186     public boolean updateDeviceStatus(ShellySettingsStatus status) throws ShellyApiException {
187         // map status to channels
188         boolean updated = false;
189         updated |= updateRelays(status);
190         updated |= updateDimmers(status);
191         updated |= updateLed(status);
192         return updated;
193     }
194
195     /**
196      * Handle Roller Commands
197      *
198      * @param command from handleCommand()
199      * @param groupName relay, roller...
200      * @param index relay number
201      * @param isControl true: is the Rollershutter channel, false: rollerpos channel
202      * @throws ShellyApiException
203      */
204     private void handleRoller(Command command, String groupName, Integer index, boolean isControl)
205             throws ShellyApiException {
206         Integer position = -1;
207
208         if ((command instanceof UpDownType) || (command instanceof OnOffType)) {
209             ShellyControlRoller rstatus = api.getRollerStatus(index);
210
211             if (!getString(rstatus.state).isEmpty() && !getString(rstatus.state).equals(SHELLY_ALWD_ROLLER_TURN_STOP)) {
212                 boolean up = command instanceof UpDownType && (UpDownType) command == UpDownType.UP;
213                 boolean down = command instanceof UpDownType && (UpDownType) command == UpDownType.DOWN;
214                 if ((up && getString(rstatus.state).equals(SHELLY_ALWD_ROLLER_TURN_OPEN))
215                         || (down && getString(rstatus.state).equals(SHELLY_ALWD_ROLLER_TURN_CLOSE))) {
216                     logger.debug("{}: Roller is already moving ({}), ignore command {}", thingName,
217                             getString(rstatus.state), command);
218                     requestUpdates(1, false);
219                     return;
220                 }
221             }
222
223             if (((command instanceof UpDownType) && UpDownType.UP.equals(command))
224                     || ((command instanceof OnOffType) && OnOffType.ON.equals(command))) {
225                 logger.debug("{}: Open roller", thingName);
226                 api.setRollerTurn(index, SHELLY_ALWD_ROLLER_TURN_OPEN);
227                 position = SHELLY_MAX_ROLLER_POS;
228
229             }
230             if (((command instanceof UpDownType) && UpDownType.DOWN.equals(command))
231                     || ((command instanceof OnOffType) && OnOffType.OFF.equals(command))) {
232                 logger.debug("{}: Closing roller", thingName);
233                 api.setRollerTurn(index, SHELLY_ALWD_ROLLER_TURN_CLOSE);
234                 position = SHELLY_MIN_ROLLER_POS;
235             }
236         } else if ((command instanceof StopMoveType) && StopMoveType.STOP.equals(command)) {
237             logger.debug("{}: Stop roller", thingName);
238             api.setRollerTurn(index, SHELLY_ALWD_ROLLER_TURN_STOP);
239         } else {
240             logger.debug("{}: Set roller to position {}", thingName, command);
241             if (command instanceof PercentType) {
242                 PercentType p = (PercentType) command;
243                 position = p.intValue();
244             } else if (command instanceof DecimalType) {
245                 DecimalType d = (DecimalType) command;
246                 position = d.intValue();
247             } else {
248                 throw new IllegalArgumentException(
249                         "Invalid value type for roller control/posiution" + command.getClass().toString());
250             }
251
252             // take position from RollerShutter control and map to Shelly positon (OH:
253             // 0=closed, 100=open; Shelly 0=open, 100=closed)
254             // take position 1:1 from position channel
255             position = isControl ? SHELLY_MAX_ROLLER_POS - position : position;
256             validateRange("roller position", position, SHELLY_MIN_ROLLER_POS, SHELLY_MAX_ROLLER_POS);
257
258             logger.debug("{}: Changing roller position to {}", thingName, position);
259             api.setRollerPos(index, position);
260         }
261         if (position != -1) {
262             // make sure both are in sync
263             if (isControl) {
264                 int pos = SHELLY_MAX_ROLLER_POS - Math.max(0, Math.min(position, SHELLY_MAX_ROLLER_POS));
265                 updateChannel(groupName, CHANNEL_ROL_CONTROL_CONTROL, new PercentType(pos));
266             } else {
267                 updateChannel(groupName, CHANNEL_ROL_CONTROL_POS, new PercentType(position));
268             }
269         }
270     }
271
272     /**
273      * Auto-create relay channels depending on relay type/mode
274      */
275     private void createRelayChannels(ShellyStatusRelay relay, int idx) {
276         if (!areChannelsCreated()) {
277             updateChannelDefinitions(ShellyChannelDefinitionsDTO.createRelayChannels(getThing(), profile, relay, idx));
278         }
279     }
280
281     private void createRollerChannels(ShellyControlRoller roller) {
282         if (!areChannelsCreated()) {
283             updateChannelDefinitions(ShellyChannelDefinitionsDTO.createRollerChannels(getThing(), roller));
284         }
285     }
286
287     /**
288      * Update Relay/Roller channels
289      *
290      * @param th Thing Handler instance
291      * @param profile ShellyDeviceProfile
292      * @param status Last ShellySettingsStatus
293      *
294      * @throws ShellyApiException
295      */
296     public boolean updateRelays(ShellySettingsStatus status) throws ShellyApiException {
297         boolean updated = false;
298         // Check for Relay in Standard Mode
299         if (profile.hasRelays && !profile.isRoller && !profile.isDimmer) {
300             logger.trace("{}: Updating {} relay(s)", thingName, profile.numRelays);
301
302             int i = 0;
303             ShellyStatusRelay rstatus = api.getRelayStatus(i);
304             for (ShellyShortStatusRelay relay : rstatus.relays) {
305                 createRelayChannels(rstatus, i);
306                 if ((relay.isValid == null) || relay.isValid) {
307                     String groupName = profile.getControlGroup(i);
308                     ShellySettingsRelay rs = profile.settings.relays.get(i);
309                     updated |= updateChannel(groupName, CHANNEL_OUTPUT_NAME, getStringType(rs.name));
310
311                     if (getBool(relay.overpower)) {
312                         postEvent(ALARM_TYPE_OVERPOWER, false);
313                     }
314
315                     updated |= updateChannel(groupName, CHANNEL_OUTPUT, getOnOff(relay.ison));
316                     updated |= updateChannel(groupName, CHANNEL_TIMER_ACTIVE, getOnOff(relay.hasTimer));
317                     if (rstatus.extTemperature != null) {
318                         // Shelly 1/1PM support up to 3 external sensors
319                         // for whatever reason those are not represented as an array, but 3 elements
320                         if (rstatus.extTemperature.sensor1 != null) {
321                             updated |= updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_ESENDOR_TEMP1, toQuantityType(
322                                     getDouble(rstatus.extTemperature.sensor1.tC), DIGITS_TEMP, SIUnits.CELSIUS));
323                         }
324                         if (rstatus.extTemperature.sensor2 != null) {
325                             updated |= updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_ESENDOR_TEMP2, toQuantityType(
326                                     getDouble(rstatus.extTemperature.sensor2.tC), DIGITS_TEMP, SIUnits.CELSIUS));
327                         }
328                         if (rstatus.extTemperature.sensor3 != null) {
329                             updated |= updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_ESENDOR_TEMP3, toQuantityType(
330                                     getDouble(rstatus.extTemperature.sensor3.tC), DIGITS_TEMP, SIUnits.CELSIUS));
331                         }
332                     }
333                     if ((rstatus.extHumidity != null) && (rstatus.extHumidity.sensor1 != null)) {
334                         updated |= updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_HUM, toQuantityType(
335                                 getDouble(rstatus.extHumidity.sensor1.hum), DIGITS_PERCENT, Units.PERCENT));
336                     }
337
338                     // Update Auto-ON/OFF timer
339                     ShellySettingsRelay rsettings = profile.settings.relays.get(i);
340                     if (rsettings != null) {
341                         updated |= updateChannel(groupName, CHANNEL_TIMER_AUTOON,
342                                 toQuantityType(getDouble(rsettings.autoOn), Units.SECOND));
343                         updated |= updateChannel(groupName, CHANNEL_TIMER_AUTOOFF,
344                                 toQuantityType(getDouble(rsettings.autoOff), Units.SECOND));
345                     }
346
347                     // Update input(s) state
348                     updated |= updateInputs(groupName, status, i);
349                 }
350                 i++;
351             }
352         }
353
354         // Check for Relay in Roller Mode
355         if (profile.hasRelays && profile.isRoller && (status.rollers != null)) {
356             logger.trace("{}: Updating {} rollers", thingName, profile.numRollers);
357             int i = 0;
358
359             for (ShellySettingsRoller roller : status.rollers) {
360                 if (roller.isValid) {
361                     ShellyControlRoller control = api.getRollerStatus(i);
362                     Integer relayIndex = i + 1;
363                     String groupName = profile.numRollers > 1 ? CHANNEL_GROUP_ROL_CONTROL + relayIndex.toString()
364                             : CHANNEL_GROUP_ROL_CONTROL;
365
366                     createRollerChannels(control);
367
368                     if (control.name != null) {
369                         updated |= updateChannel(groupName, CHANNEL_OUTPUT_NAME, getStringType(control.name));
370                     }
371
372                     String state = getString(control.state);
373                     if (state.equals(SHELLY_ALWD_ROLLER_TURN_STOP)) { // only valid in stop state
374                         int pos = Math.max(SHELLY_MIN_ROLLER_POS, Math.min(control.currentPos, SHELLY_MAX_ROLLER_POS));
375                         updated |= updateChannel(groupName, CHANNEL_ROL_CONTROL_CONTROL,
376                                 toQuantityType(new Double(SHELLY_MAX_ROLLER_POS - pos), Units.PERCENT));
377                         updated |= updateChannel(groupName, CHANNEL_ROL_CONTROL_POS,
378                                 toQuantityType(new Double(pos), Units.PERCENT));
379                         scheduledUpdates = 1; // one more poll and then stop
380                     }
381
382                     updated |= updateChannel(groupName, CHANNEL_ROL_CONTROL_STATE, new StringType(state));
383                     updated |= updateChannel(groupName, CHANNEL_ROL_CONTROL_STOPR, getStringType(control.stopReason));
384                     updated |= updateInputs(groupName, status, i);
385
386                     i++;
387                 }
388             }
389         }
390         return updated;
391     }
392
393     /**
394      * Update Relay/Roller channels
395      *
396      * @param th Thing Handler instance
397      * @param profile ShellyDeviceProfile
398      * @param status Last ShellySettingsStatus
399      *
400      * @throws ShellyApiException
401      */
402     public boolean updateDimmers(ShellySettingsStatus orgStatus) throws ShellyApiException {
403         boolean updated = false;
404         if (profile.isDimmer) {
405             // We need to fixup the returned Json: The dimmer returns light[] element, which is ok, but it doesn't have
406             // the same structure as lights[] from Bulb,RGBW2 and Duo. The tag gets replaced by dimmers[] so that Gson
407             // maps to a different structure (ShellyShortLight).
408             Gson gson = new Gson();
409             ShellySettingsStatus dstatus = gson.fromJson(ShellyApiJsonDTO.fixDimmerJson(orgStatus.json),
410                     ShellySettingsStatus.class);
411
412             logger.trace("{}: Updating {} dimmers(s)", thingName, dstatus.dimmers.size());
413             int l = 0;
414             for (ShellyShortLightStatus dimmer : dstatus.dimmers) {
415                 Integer r = l + 1;
416                 String groupName = profile.numRelays <= 1 ? CHANNEL_GROUP_DIMMER_CONTROL
417                         : CHANNEL_GROUP_DIMMER_CONTROL + r.toString();
418
419                 // On a status update we map a dimmer.ison = false to brightness 0 rather than the device's brightness
420                 // and send a OFF status to the same channel.
421                 // When the device's brightness is > 0 we send the new value to the channel and a ON command
422                 if (dimmer.ison) {
423                     updated |= updateChannel(groupName, CHANNEL_BRIGHTNESS + "$Switch", OnOffType.ON);
424                     updated |= updateChannel(groupName, CHANNEL_BRIGHTNESS + "$Value",
425                             toQuantityType(new Double(getInteger(dimmer.brightness)), DIGITS_NONE, Units.PERCENT));
426                 } else {
427                     updated |= updateChannel(groupName, CHANNEL_BRIGHTNESS + "$Switch", OnOffType.OFF);
428                     updated |= updateChannel(groupName, CHANNEL_BRIGHTNESS + "$Value",
429                             toQuantityType(new Double(0), DIGITS_NONE, Units.PERCENT));
430                 }
431
432                 ShellySettingsDimmer dsettings = profile.settings.dimmers.get(l);
433                 if (dsettings != null) {
434                     updated |= updateChannel(groupName, CHANNEL_TIMER_AUTOON,
435                             toQuantityType(getDouble(dsettings.autoOn), Units.SECOND));
436                     updated |= updateChannel(groupName, CHANNEL_TIMER_AUTOOFF,
437                             toQuantityType(getDouble(dsettings.autoOff), Units.SECOND));
438                 }
439
440                 updated |= updateInputs(groupName, orgStatus, l);
441                 l++;
442             }
443         }
444         return updated;
445     }
446
447     /**
448      * Update LED channels
449      *
450      * @param th Thing Handler instance
451      * @param profile ShellyDeviceProfile
452      * @param status Last ShellySettingsStatus
453      */
454     public boolean updateLed(ShellySettingsStatus status) {
455         boolean updated = false;
456         updated |= updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_LED_STATUS_DISABLE,
457                 getOnOff(profile.settings.ledStatusDisable));
458         updated |= updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_LED_POWER_DISABLE,
459                 getOnOff(profile.settings.ledPowerDisable));
460         return updated;
461     }
462 }