2 * Copyright (c) 2010-2021 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.shelly.internal.handler;
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.*;
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.provider.ShellyChannelDefinitions;
34 import org.openhab.binding.shelly.internal.provider.ShellyTranslationProvider;
35 import org.openhab.core.library.types.DecimalType;
36 import org.openhab.core.library.types.IncreaseDecreaseType;
37 import org.openhab.core.library.types.OnOffType;
38 import org.openhab.core.library.types.PercentType;
39 import org.openhab.core.library.types.StopMoveType;
40 import org.openhab.core.library.types.StringType;
41 import org.openhab.core.library.types.UpDownType;
42 import org.openhab.core.library.unit.SIUnits;
43 import org.openhab.core.library.unit.Units;
44 import org.openhab.core.thing.ChannelUID;
45 import org.openhab.core.thing.Thing;
46 import org.openhab.core.types.Command;
47 import org.slf4j.Logger;
48 import org.slf4j.LoggerFactory;
50 import com.google.gson.Gson;
53 * The{@link ShellyRelayHandler} handles light (bulb+rgbw2) specific commands and status. All other commands will be
54 * handled by the generic thing handler.
56 * @author Markus Michels - Initial contribution
59 public class ShellyRelayHandler extends ShellyBaseHandler {
60 private final Logger logger = LoggerFactory.getLogger(ShellyRelayHandler.class);
65 * @param thing The thing passed by the HandlerFactory
66 * @param bindingConfig configuration of the binding
67 * @param coapServer coap server instance
68 * @param localIP local IP of the openHAB host
69 * @param httpPort port of the openHAB HTTP API
71 public ShellyRelayHandler(final Thing thing, final ShellyTranslationProvider translationProvider,
72 final ShellyBindingConfiguration bindingConfig, final ShellyCoapServer coapServer, final String localIP,
73 int httpPort, final HttpClient httpClient) {
74 super(thing, translationProvider, bindingConfig, coapServer, localIP, httpPort, httpClient);
78 public void initialize() {
83 public boolean handleDeviceCommand(ChannelUID channelUID, Command command) throws ShellyApiException {
85 String groupName = getString(channelUID.getGroupId());
87 if (groupName.startsWith(CHANNEL_GROUP_RELAY_CONTROL)
88 && groupName.length() > CHANNEL_GROUP_RELAY_CONTROL.length()) {
89 rIndex = Integer.parseInt(substringAfter(channelUID.getGroupId(), CHANNEL_GROUP_RELAY_CONTROL)) - 1;
90 } else if (groupName.startsWith(CHANNEL_GROUP_ROL_CONTROL)
91 && groupName.length() > CHANNEL_GROUP_ROL_CONTROL.length()) {
92 rIndex = Integer.parseInt(substringAfter(channelUID.getGroupId(), CHANNEL_GROUP_ROL_CONTROL)) - 1;
95 switch (channelUID.getIdWithoutGroup()) {
100 if (!profile.isRoller) {
101 // extract relay number of group name (relay0->0, relay1->1...)
102 logger.debug("{}: Set relay output to {}", thingName, command);
103 api.setRelayTurn(rIndex, command == OnOffType.ON ? SHELLY_API_ON : SHELLY_API_OFF);
105 logger.debug("{}: Device is in roller mode, channel command {} ignored", thingName, channelUID);
108 case CHANNEL_BRIGHTNESS: // e.g.Dimmer, Duo
109 handleBrightness(command, rIndex);
112 case CHANNEL_ROL_CONTROL_POS:
113 case CHANNEL_ROL_CONTROL_CONTROL:
114 logger.debug("{}: Roller command/position {}", thingName, command);
115 handleRoller(command, groupName, rIndex,
116 channelUID.getIdWithoutGroup().equals(CHANNEL_ROL_CONTROL_CONTROL));
118 // request updates the next 45sec to update roller position after it stopped
119 requestUpdates(autoCoIoT ? 1 : 45 / UPDATE_STATUS_INTERVAL_SECONDS, false);
122 case CHANNEL_ROL_CONTROL_FAV:
123 if (command instanceof Number) {
124 int id = ((Number) command).intValue() - 1;
125 int pos = profile.getRollerFav(id);
127 logger.debug("{}: Selecting favorite {}, position = {}", thingName, id, pos);
128 api.setRollerPos(rIndex, pos);
132 logger.debug("{}: Invalid favorite index: {}", thingName, command);
135 case CHANNEL_TIMER_AUTOON:
136 logger.debug("{}: Set Auto-ON timer to {}", thingName, command);
137 api.setTimer(rIndex, SHELLY_TIMER_AUTOON, getNumber(command));
139 case CHANNEL_TIMER_AUTOOFF:
140 logger.debug("{}: Set Auto-OFF timer to {}", thingName, command);
141 api.setTimer(rIndex, SHELLY_TIMER_AUTOOFF, getNumber(command));
148 * Brightness channel has 2 functions: Switch On/Off (OnOnType) and setting brightness (PercentType)
149 * There is some more logic in the control. When brightness is set to 0 the control sends also an OFF command
150 * When current brightness is 0 and slider will be moved the new brightness will be set, but also a ON command is
155 * @throws ShellyApiException
157 private void handleBrightness(Command command, Integer index) throws ShellyApiException {
159 if (command instanceof PercentType) { // Dimmer
160 value = ((PercentType) command).intValue();
161 } else if (command instanceof DecimalType) { // Number
162 value = ((DecimalType) command).intValue();
163 } else if (command instanceof OnOffType) { // Switch
164 logger.debug("{}: Switch output {}", thingName, command);
165 updateBrightnessChannel(index, (OnOffType) command, value);
167 } else if (command instanceof IncreaseDecreaseType) {
168 ShellyShortLightStatus light = api.getLightStatus(index);
169 if (command == IncreaseDecreaseType.INCREASE) {
170 value = Math.min(light.brightness + DIM_STEPSIZE, 100);
172 value = Math.max(light.brightness - DIM_STEPSIZE, 0);
174 logger.debug("{}: Increase/Decrease brightness from {} to {}", thingName, light.brightness, value);
176 validateRange("brightness", value, 0, 100);
178 // Switch light off on brightness = 0
180 logger.debug("{}: Brightness=0 -> switch output OFF", thingName);
181 updateBrightnessChannel(index, OnOffType.OFF, 0);
183 logger.debug("{}: Setting dimmer brightness to {}", thingName, value);
184 updateBrightnessChannel(index, OnOffType.ON, value);
188 private void updateBrightnessChannel(int lightId, OnOffType power, int brightness) throws ShellyApiException {
189 updateChannel(CHANNEL_COLOR_WHITE, CHANNEL_BRIGHTNESS + "$Switch", power);
190 if (brightness > 0) {
191 api.setBrightness(lightId, brightness, config.brightnessAutoOn);
193 api.setRelayTurn(lightId, power == OnOffType.ON ? SHELLY_API_ON : SHELLY_API_OFF);
194 if (brightness >= 0) { // ignore -1
195 updateChannel(CHANNEL_COLOR_WHITE, CHANNEL_BRIGHTNESS + "$Value",
196 toQuantityType((double) (power == OnOffType.ON ? brightness : 0), DIGITS_NONE, Units.PERCENT));
202 public boolean updateDeviceStatus(ShellySettingsStatus status) throws ShellyApiException {
203 // map status to channels
204 boolean updated = false;
205 updated |= updateRelays(status);
206 updated |= updateDimmers(status);
207 updated |= updateLed(status);
212 * Handle Roller Commands
214 * @param command from handleCommand()
215 * @param groupName relay, roller...
216 * @param index relay number
217 * @param isControl true: is the Rollershutter channel, false: rollerpos channel
218 * @throws ShellyApiException
220 private void handleRoller(Command command, String groupName, Integer index, boolean isControl)
221 throws ShellyApiException {
222 Integer position = -1;
224 if ((command instanceof UpDownType) || (command instanceof OnOffType)) {
225 ShellyControlRoller rstatus = api.getRollerStatus(index);
227 if (!getString(rstatus.state).isEmpty() && !getString(rstatus.state).equals(SHELLY_ALWD_ROLLER_TURN_STOP)) {
228 if ((command == UpDownType.UP && getString(rstatus.state).equals(SHELLY_ALWD_ROLLER_TURN_OPEN))
229 || (command == UpDownType.DOWN
230 && getString(rstatus.state).equals(SHELLY_ALWD_ROLLER_TURN_CLOSE))) {
231 logger.debug("{}: Roller is already moving ({}), ignore command {}", thingName,
232 getString(rstatus.state), command);
233 requestUpdates(1, false);
238 if (command == UpDownType.UP || command == OnOffType.ON) {
239 logger.debug("{}: Open roller", thingName);
240 api.setRollerTurn(index, SHELLY_ALWD_ROLLER_TURN_OPEN);
241 int pos = profile.getRollerFav(config.favoriteUP - 1);
242 position = pos > 0 ? pos : SHELLY_MAX_ROLLER_POS;
244 logger.debug("{}: Use favoriteUP id {} for positioning roller({}%)", thingName, config.favoriteUP,
247 } else if (command == UpDownType.DOWN || command == OnOffType.OFF) {
248 logger.debug("{}: Closing roller", thingName);
249 int pos = profile.getRollerFav(config.favoriteDOWN - 1);
251 // use favorite position
253 logger.debug("{}: Use favoriteDOWN id {} for positioning roller ({}%)", thingName,
254 config.favoriteDOWN, pos);
256 api.setRollerPos(index, pos);
258 api.setRollerTurn(index, SHELLY_ALWD_ROLLER_TURN_CLOSE);
260 position = SHELLY_MAX_ROLLER_POS - pos;
262 } else if (command == StopMoveType.STOP) {
263 logger.debug("{}: Stop roller", thingName);
264 api.setRollerTurn(index, SHELLY_ALWD_ROLLER_TURN_STOP);
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();
274 throw new IllegalArgumentException(
275 "Invalid value type for roller control/position" + command.getClass().toString());
278 // take position from RollerShutter control and map to Shelly positon (OH:
279 // 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);
284 logger.debug("{}: Changing roller position to {}", thingName, position);
285 api.setRollerPos(index, position);
287 if (position != -1) {
288 // make sure both are in sync
290 int pos = SHELLY_MAX_ROLLER_POS - Math.max(0, Math.min(position, SHELLY_MAX_ROLLER_POS));
291 updateChannel(groupName, CHANNEL_ROL_CONTROL_CONTROL, new PercentType(pos));
293 updateChannel(groupName, CHANNEL_ROL_CONTROL_POS, new PercentType(position));
299 * Auto-create relay channels depending on relay type/mode
301 private void createRelayChannels(ShellyStatusRelay relay, int idx) {
302 if (!areChannelsCreated()) {
303 updateChannelDefinitions(ShellyChannelDefinitions.createRelayChannels(getThing(), profile, relay, idx));
307 private void createDimmerChannels(ShellySettingsStatus dstatus, int idx) {
308 if (!areChannelsCreated()) {
309 updateChannelDefinitions(ShellyChannelDefinitions.createDimmerChannels(getThing(), profile, dstatus, idx));
313 private void createRollerChannels(ShellyControlRoller roller) {
314 if (!areChannelsCreated()) {
315 updateChannelDefinitions(ShellyChannelDefinitions.createRollerChannels(getThing(), roller));
320 * Update Relay/Roller channels
322 * @param th Thing Handler instance
323 * @param profile ShellyDeviceProfile
324 * @param status Last ShellySettingsStatus
326 * @throws ShellyApiException
328 public boolean updateRelays(ShellySettingsStatus status) throws ShellyApiException {
329 boolean updated = false;
330 // Check for Relay in Standard Mode
331 if (profile.hasRelays && !profile.isRoller && !profile.isDimmer) {
332 logger.trace("{}: Updating {} relay(s)", thingName, profile.numRelays);
335 ShellyStatusRelay rstatus = api.getRelayStatus(i);
336 for (ShellyShortStatusRelay relay : rstatus.relays) {
337 createRelayChannels(rstatus, i);
338 if ((relay.isValid == null) || relay.isValid) {
339 String groupName = profile.getControlGroup(i);
340 ShellySettingsRelay rs = profile.settings.relays.get(i);
341 updated |= updateChannel(groupName, CHANNEL_OUTPUT_NAME, getStringType(rs.name));
343 if (getBool(relay.overpower)) {
344 postEvent(ALARM_TYPE_OVERPOWER, false);
347 updated |= updateChannel(groupName, CHANNEL_OUTPUT, getOnOff(relay.ison));
348 updated |= updateChannel(groupName, CHANNEL_TIMER_ACTIVE, getOnOff(relay.hasTimer));
349 if (rstatus.extTemperature != null) {
350 // Shelly 1/1PM support up to 3 external sensors
351 // for whatever reason those are not represented as an array, but 3 elements
352 if (rstatus.extTemperature.sensor1 != null) {
353 updated |= updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_ESENDOR_TEMP1, toQuantityType(
354 getDouble(rstatus.extTemperature.sensor1.tC), DIGITS_TEMP, SIUnits.CELSIUS));
356 if (rstatus.extTemperature.sensor2 != null) {
357 updated |= updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_ESENDOR_TEMP2, toQuantityType(
358 getDouble(rstatus.extTemperature.sensor2.tC), DIGITS_TEMP, SIUnits.CELSIUS));
360 if (rstatus.extTemperature.sensor3 != null) {
361 updated |= updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_ESENDOR_TEMP3, toQuantityType(
362 getDouble(rstatus.extTemperature.sensor3.tC), DIGITS_TEMP, SIUnits.CELSIUS));
365 if ((rstatus.extHumidity != null) && (rstatus.extHumidity.sensor1 != null)) {
366 updated |= updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_HUM, toQuantityType(
367 getDouble(rstatus.extHumidity.sensor1.hum), DIGITS_PERCENT, Units.PERCENT));
370 // Update Auto-ON/OFF timer
371 ShellySettingsRelay rsettings = profile.settings.relays.get(i);
372 if (rsettings != null) {
373 updated |= updateChannel(groupName, CHANNEL_TIMER_AUTOON,
374 toQuantityType(getDouble(rsettings.autoOn), Units.SECOND));
375 updated |= updateChannel(groupName, CHANNEL_TIMER_AUTOOFF,
376 toQuantityType(getDouble(rsettings.autoOff), Units.SECOND));
381 } else if (profile.hasRelays && profile.isRoller && (status.rollers != null)) {
382 // Check for Relay in Roller Mode
383 logger.trace("{}: Updating {} rollers", thingName, profile.numRollers);
386 for (ShellySettingsRoller roller : status.rollers) {
387 if (roller.isValid) {
388 ShellyControlRoller control = api.getRollerStatus(i);
389 Integer relayIndex = i + 1;
390 String groupName = profile.numRollers > 1 ? CHANNEL_GROUP_ROL_CONTROL + relayIndex.toString()
391 : CHANNEL_GROUP_ROL_CONTROL;
393 createRollerChannels(control);
395 if (control.name != null) {
396 updated |= updateChannel(groupName, CHANNEL_OUTPUT_NAME, getStringType(control.name));
399 String state = getString(control.state);
400 if (state.equals(SHELLY_ALWD_ROLLER_TURN_STOP)) { // only valid in stop state
401 int pos = Math.max(SHELLY_MIN_ROLLER_POS, Math.min(control.currentPos, SHELLY_MAX_ROLLER_POS));
402 updated |= updateChannel(groupName, CHANNEL_ROL_CONTROL_CONTROL,
403 toQuantityType((double) (SHELLY_MAX_ROLLER_POS - pos), Units.PERCENT));
404 updated |= updateChannel(groupName, CHANNEL_ROL_CONTROL_POS,
405 toQuantityType((double) pos, Units.PERCENT));
406 scheduledUpdates = 1; // one more poll and then stop
409 updated |= updateChannel(groupName, CHANNEL_ROL_CONTROL_STATE, new StringType(state));
410 updated |= updateChannel(groupName, CHANNEL_ROL_CONTROL_STOPR, getStringType(control.stopReason));
411 updated |= updateChannel(groupName, CHANNEL_ROL_CONTROL_SAFETY, getOnOff(control.safetySwitch));
421 * Update Relay/Roller channels
423 * @param th Thing Handler instance
424 * @param profile ShellyDeviceProfile
425 * @param status Last ShellySettingsStatus
427 * @throws ShellyApiException
429 public boolean updateDimmers(ShellySettingsStatus orgStatus) throws ShellyApiException {
430 boolean updated = false;
431 if (profile.isDimmer) {
432 // We need to fixup the returned Json: The dimmer returns light[] element, which is ok, but it doesn't have
433 // the same structure as lights[] from Bulb,RGBW2 and Duo. The tag gets replaced by dimmers[] so that Gson
434 // maps to a different structure (ShellyShortLight).
435 Gson gson = new Gson();
436 ShellySettingsStatus dstatus = fromJson(gson, ShellyApiJsonDTO.fixDimmerJson(orgStatus.json),
437 ShellySettingsStatus.class);
439 logger.trace("{}: Updating {} dimmers(s)", thingName, dstatus.dimmers.size());
441 for (ShellyShortLightStatus dimmer : dstatus.dimmers) {
443 String groupName = profile.numRelays <= 1 ? CHANNEL_GROUP_DIMMER_CONTROL
444 : CHANNEL_GROUP_DIMMER_CONTROL + r.toString();
446 createDimmerChannels(dstatus, l);
448 // On a status update we map a dimmer.ison = false to brightness 0 rather than the device's brightness
449 // and send a OFF status to the same channel.
450 // When the device's brightness is > 0 we send the new value to the channel and a ON command
452 updated |= updateChannel(groupName, CHANNEL_BRIGHTNESS + "$Switch", OnOffType.ON);
453 updated |= updateChannel(groupName, CHANNEL_BRIGHTNESS + "$Value",
454 toQuantityType((double) getInteger(dimmer.brightness), DIGITS_NONE, Units.PERCENT));
456 updated |= updateChannel(groupName, CHANNEL_BRIGHTNESS + "$Switch", OnOffType.OFF);
457 updated |= updateChannel(groupName, CHANNEL_BRIGHTNESS + "$Value",
458 toQuantityType(0.0, DIGITS_NONE, Units.PERCENT));
461 ShellySettingsDimmer dsettings = profile.settings.dimmers.get(l);
462 if (dsettings != null) {
463 updated |= updateChannel(groupName, CHANNEL_TIMER_AUTOON,
464 toQuantityType(getDouble(dsettings.autoOn), Units.SECOND));
465 updated |= updateChannel(groupName, CHANNEL_TIMER_AUTOOFF,
466 toQuantityType(getDouble(dsettings.autoOff), Units.SECOND));
476 * Update LED channels
478 * @param th Thing Handler instance
479 * @param profile ShellyDeviceProfile
480 * @param status Last ShellySettingsStatus
482 public boolean updateLed(ShellySettingsStatus status) {
483 boolean updated = false;
484 updated |= updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_LED_STATUS_DISABLE,
485 getOnOff(profile.settings.ledStatusDisable));
486 updated |= updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_LED_POWER_DISABLE,
487 getOnOff(profile.settings.ledPowerDisable));