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).intValue());
139 case CHANNEL_TIMER_AUTOOFF:
140 logger.debug("{}: Set Auto-OFF timer to {}", thingName, command);
141 api.setTimer(rIndex, SHELLY_TIMER_AUTOOFF, getNumber(command).intValue());
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 {
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 || ((command instanceof DecimalType) && (((DecimalType) command).intValue() == 100))) {
240 logger.debug("{}: Open roller", thingName);
241 int shpos = profile.getRollerFav(config.favoriteUP - 1);
243 logger.debug("{}: Use favoriteUP id {} for positioning roller({}%)", thingName, config.favoriteUP,
245 api.setRollerPos(index, shpos);
248 api.setRollerTurn(index, SHELLY_ALWD_ROLLER_TURN_OPEN);
249 position = SHELLY_MIN_ROLLER_POS;
251 } else if (command == UpDownType.DOWN || command == OnOffType.OFF
252 || ((command instanceof DecimalType) && (((DecimalType) command).intValue() == 0))) {
253 logger.debug("{}: Closing roller", thingName);
254 int shpos = profile.getRollerFav(config.favoriteDOWN - 1);
256 // use favorite position
257 logger.debug("{}: Use favoriteDOWN id {} for positioning roller ({}%)", thingName,
258 config.favoriteDOWN, shpos);
259 api.setRollerPos(index, shpos);
262 api.setRollerTurn(index, SHELLY_ALWD_ROLLER_TURN_CLOSE);
263 position = SHELLY_MAX_ROLLER_POS;
266 } else if (command == StopMoveType.STOP) {
267 logger.debug("{}: Stop roller", thingName);
268 api.setRollerTurn(index, SHELLY_ALWD_ROLLER_TURN_STOP);
270 logger.debug("{}: Set roller to position {}", thingName, command);
271 if (command instanceof PercentType) {
272 PercentType p = (PercentType) command;
273 position = p.intValue();
274 } else if (command instanceof DecimalType) {
275 DecimalType d = (DecimalType) command;
276 position = d.intValue();
278 throw new IllegalArgumentException(
279 "Invalid value type for roller control/position" + command.getClass().toString());
282 // take position from RollerShutter control and map to Shelly positon
283 // OH: 0=closed, 100=open; Shelly 0=open, 100=closed)
284 // take position 1:1 from position channel
285 position = isControl ? SHELLY_MAX_ROLLER_POS - position : position;
286 validateRange("roller position", position, SHELLY_MIN_ROLLER_POS, SHELLY_MAX_ROLLER_POS);
288 logger.debug("{}: Changing roller position to {}", thingName, position);
289 api.setRollerPos(index, position);
292 if (position != -1) {
293 // make sure both are in sync
295 int pos = SHELLY_MAX_ROLLER_POS - Math.max(0, Math.min(position, SHELLY_MAX_ROLLER_POS));
296 logger.debug("{}: Set roller position for control channel to {}", thingName, pos);
297 updateChannel(groupName, CHANNEL_ROL_CONTROL_CONTROL, new PercentType(pos));
299 logger.debug("{}: Set roller position channel to {}", thingName, position);
300 updateChannel(groupName, CHANNEL_ROL_CONTROL_POS, new PercentType(position));
306 * Auto-create relay channels depending on relay type/mode
308 private void createRelayChannels(ShellyStatusRelay relay, int idx) {
309 if (!areChannelsCreated()) {
310 updateChannelDefinitions(ShellyChannelDefinitions.createRelayChannels(getThing(), profile, relay, idx));
314 private void createDimmerChannels(ShellySettingsStatus dstatus, int idx) {
315 if (!areChannelsCreated()) {
316 updateChannelDefinitions(ShellyChannelDefinitions.createDimmerChannels(getThing(), profile, dstatus, idx));
320 private void createRollerChannels(ShellyControlRoller roller) {
321 if (!areChannelsCreated()) {
322 updateChannelDefinitions(ShellyChannelDefinitions.createRollerChannels(getThing(), roller));
327 * Update Relay/Roller channels
329 * @param th Thing Handler instance
330 * @param profile ShellyDeviceProfile
331 * @param status Last ShellySettingsStatus
333 * @throws ShellyApiException
335 public boolean updateRelays(ShellySettingsStatus status) throws ShellyApiException {
336 boolean updated = false;
337 // Check for Relay in Standard Mode
338 if (profile.hasRelays && !profile.isRoller && !profile.isDimmer) {
339 logger.trace("{}: Updating {} relay(s)", thingName, profile.numRelays);
342 ShellyStatusRelay rstatus = api.getRelayStatus(i);
343 for (ShellyShortStatusRelay relay : rstatus.relays) {
344 createRelayChannels(rstatus, i);
345 if ((relay.isValid == null) || relay.isValid) {
346 String groupName = profile.getControlGroup(i);
347 ShellySettingsRelay rs = profile.settings.relays.get(i);
348 updated |= updateChannel(groupName, CHANNEL_OUTPUT_NAME, getStringType(rs.name));
350 if (getBool(relay.overpower)) {
351 postEvent(ALARM_TYPE_OVERPOWER, false);
354 updated |= updateChannel(groupName, CHANNEL_OUTPUT, getOnOff(relay.ison));
355 updated |= updateChannel(groupName, CHANNEL_TIMER_ACTIVE, getOnOff(relay.hasTimer));
356 if (rstatus.extTemperature != null) {
357 // Shelly 1/1PM support up to 3 external sensors
358 // for whatever reason those are not represented as an array, but 3 elements
359 if (rstatus.extTemperature.sensor1 != null) {
360 updated |= updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_ESENDOR_TEMP1, toQuantityType(
361 getDouble(rstatus.extTemperature.sensor1.tC), DIGITS_TEMP, SIUnits.CELSIUS));
363 if (rstatus.extTemperature.sensor2 != null) {
364 updated |= updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_ESENDOR_TEMP2, toQuantityType(
365 getDouble(rstatus.extTemperature.sensor2.tC), DIGITS_TEMP, SIUnits.CELSIUS));
367 if (rstatus.extTemperature.sensor3 != null) {
368 updated |= updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_ESENDOR_TEMP3, toQuantityType(
369 getDouble(rstatus.extTemperature.sensor3.tC), DIGITS_TEMP, SIUnits.CELSIUS));
372 if ((rstatus.extHumidity != null) && (rstatus.extHumidity.sensor1 != null)) {
373 updated |= updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_HUM, toQuantityType(
374 getDouble(rstatus.extHumidity.sensor1.hum), DIGITS_PERCENT, Units.PERCENT));
377 // Update Auto-ON/OFF timer
378 ShellySettingsRelay rsettings = profile.settings.relays.get(i);
379 if (rsettings != null) {
380 updated |= updateChannel(groupName, CHANNEL_TIMER_AUTOON,
381 toQuantityType(getDouble(rsettings.autoOn), Units.SECOND));
382 updated |= updateChannel(groupName, CHANNEL_TIMER_AUTOOFF,
383 toQuantityType(getDouble(rsettings.autoOff), Units.SECOND));
388 } else if (profile.hasRelays && profile.isRoller && (status.rollers != null)) {
389 // Check for Relay in Roller Mode
390 logger.trace("{}: Updating {} rollers", thingName, profile.numRollers);
393 for (ShellySettingsRoller roller : status.rollers) {
394 if (roller.isValid) {
395 ShellyControlRoller control = api.getRollerStatus(i);
396 Integer relayIndex = i + 1;
397 String groupName = profile.numRollers > 1 ? CHANNEL_GROUP_ROL_CONTROL + relayIndex.toString()
398 : CHANNEL_GROUP_ROL_CONTROL;
400 createRollerChannels(control);
402 if (control.name != null) {
403 updated |= updateChannel(groupName, CHANNEL_OUTPUT_NAME, getStringType(control.name));
406 String state = getString(control.state);
407 if (state.equals(SHELLY_ALWD_ROLLER_TURN_STOP)) { // only valid in stop state
408 int pos = Math.max(SHELLY_MIN_ROLLER_POS, Math.min(control.currentPos, SHELLY_MAX_ROLLER_POS));
409 logger.debug("{}: REST Update roller position: control={}, position={}", thingName,
410 SHELLY_MAX_ROLLER_POS - pos, pos);
411 updated |= updateChannel(groupName, CHANNEL_ROL_CONTROL_CONTROL,
412 toQuantityType((double) (SHELLY_MAX_ROLLER_POS - pos), Units.PERCENT));
413 updated |= updateChannel(groupName, CHANNEL_ROL_CONTROL_POS,
414 toQuantityType((double) pos, Units.PERCENT));
415 scheduledUpdates = 1; // one more poll and then stop
418 updated |= updateChannel(groupName, CHANNEL_ROL_CONTROL_STATE, new StringType(state));
419 updated |= updateChannel(groupName, CHANNEL_ROL_CONTROL_STOPR, getStringType(control.stopReason));
420 updated |= updateChannel(groupName, CHANNEL_ROL_CONTROL_SAFETY, getOnOff(control.safetySwitch));
430 * Update Relay/Roller channels
432 * @param th Thing Handler instance
433 * @param profile ShellyDeviceProfile
434 * @param status Last ShellySettingsStatus
436 * @throws ShellyApiException
438 public boolean updateDimmers(ShellySettingsStatus orgStatus) throws ShellyApiException {
439 boolean updated = false;
440 if (profile.isDimmer) {
441 // We need to fixup the returned Json: The dimmer returns light[] element, which is ok, but it doesn't have
442 // the same structure as lights[] from Bulb,RGBW2 and Duo. The tag gets replaced by dimmers[] so that Gson
443 // maps to a different structure (ShellyShortLight).
444 Gson gson = new Gson();
445 ShellySettingsStatus dstatus = fromJson(gson, ShellyApiJsonDTO.fixDimmerJson(orgStatus.json),
446 ShellySettingsStatus.class);
448 logger.trace("{}: Updating {} dimmers(s)", thingName, dstatus.dimmers.size());
450 for (ShellyShortLightStatus dimmer : dstatus.dimmers) {
452 String groupName = profile.numRelays <= 1 ? CHANNEL_GROUP_DIMMER_CONTROL
453 : CHANNEL_GROUP_DIMMER_CONTROL + r.toString();
455 createDimmerChannels(dstatus, l);
457 // On a status update we map a dimmer.ison = false to brightness 0 rather than the device's brightness
458 // and send a OFF status to the same channel.
459 // When the device's brightness is > 0 we send the new value to the channel and a ON command
461 updated |= updateChannel(groupName, CHANNEL_BRIGHTNESS + "$Switch", OnOffType.ON);
462 updated |= updateChannel(groupName, CHANNEL_BRIGHTNESS + "$Value",
463 toQuantityType((double) getInteger(dimmer.brightness), DIGITS_NONE, Units.PERCENT));
465 updated |= updateChannel(groupName, CHANNEL_BRIGHTNESS + "$Switch", OnOffType.OFF);
466 updated |= updateChannel(groupName, CHANNEL_BRIGHTNESS + "$Value",
467 toQuantityType(0.0, DIGITS_NONE, Units.PERCENT));
470 ShellySettingsDimmer dsettings = profile.settings.dimmers.get(l);
471 if (dsettings != null) {
472 updated |= updateChannel(groupName, CHANNEL_TIMER_AUTOON,
473 toQuantityType(getDouble(dsettings.autoOn), Units.SECOND));
474 updated |= updateChannel(groupName, CHANNEL_TIMER_AUTOOFF,
475 toQuantityType(getDouble(dsettings.autoOff), Units.SECOND));
485 * Update LED channels
487 * @param th Thing Handler instance
488 * @param profile ShellyDeviceProfile
489 * @param status Last ShellySettingsStatus
491 public boolean updateLed(ShellySettingsStatus status) {
492 boolean updated = false;
493 updated |= updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_LED_STATUS_DISABLE,
494 getOnOff(profile.settings.ledStatusDisable));
495 updated |= updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_LED_POWER_DISABLE,
496 getOnOff(profile.settings.ledPowerDisable));