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) {
340 if (status.voltage == null && profile.settings.supplyVoltage != null) {
341 // Shelly 1PM/1L (fix)
342 voltage = profile.settings.supplyVoltage == 0 ? 110.0 : 220.0;
344 // Shelly 2.5 (measured)
345 voltage = getDouble(status.voltage);
348 updated |= updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_VOLTAGE,
349 toQuantityType(voltage, DIGITS_VOLT, Units.VOLT));
352 logger.trace("{}: Updating {} relay(s)", thingName, profile.numRelays);
354 ShellyStatusRelay rstatus = api.getRelayStatus(i);
355 for (ShellyShortStatusRelay relay : rstatus.relays) {
356 createRelayChannels(rstatus, i);
357 if ((relay.isValid == null) || relay.isValid) {
358 String groupName = profile.getControlGroup(i);
359 ShellySettingsRelay rs = profile.settings.relays.get(i);
360 updated |= updateChannel(groupName, CHANNEL_OUTPUT_NAME, getStringType(rs.name));
362 if (getBool(relay.overpower)) {
363 postEvent(ALARM_TYPE_OVERPOWER, false);
366 updated |= updateChannel(groupName, CHANNEL_OUTPUT, getOnOff(relay.ison));
367 updated |= updateChannel(groupName, CHANNEL_TIMER_ACTIVE, getOnOff(relay.hasTimer));
368 if (rstatus.extTemperature != null) {
369 // Shelly 1/1PM support up to 3 external sensors
370 // for whatever reason those are not represented as an array, but 3 elements
371 if (rstatus.extTemperature.sensor1 != null) {
372 updated |= updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_ESENDOR_TEMP1, toQuantityType(
373 getDouble(rstatus.extTemperature.sensor1.tC), DIGITS_TEMP, SIUnits.CELSIUS));
375 if (rstatus.extTemperature.sensor2 != null) {
376 updated |= updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_ESENDOR_TEMP2, toQuantityType(
377 getDouble(rstatus.extTemperature.sensor2.tC), DIGITS_TEMP, SIUnits.CELSIUS));
379 if (rstatus.extTemperature.sensor3 != null) {
380 updated |= updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_ESENDOR_TEMP3, toQuantityType(
381 getDouble(rstatus.extTemperature.sensor3.tC), DIGITS_TEMP, SIUnits.CELSIUS));
384 if ((rstatus.extHumidity != null) && (rstatus.extHumidity.sensor1 != null)) {
385 updated |= updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_HUM, toQuantityType(
386 getDouble(rstatus.extHumidity.sensor1.hum), DIGITS_PERCENT, Units.PERCENT));
389 // Update Auto-ON/OFF timer
390 ShellySettingsRelay rsettings = profile.settings.relays.get(i);
391 if (rsettings != null) {
392 updated |= updateChannel(groupName, CHANNEL_TIMER_AUTOON,
393 toQuantityType(getDouble(rsettings.autoOn), Units.SECOND));
394 updated |= updateChannel(groupName, CHANNEL_TIMER_AUTOOFF,
395 toQuantityType(getDouble(rsettings.autoOff), Units.SECOND));
400 } else if (profile.hasRelays && profile.isRoller && (status.rollers != null)) {
401 // Check for Relay in Roller Mode
402 logger.trace("{}: Updating {} rollers", thingName, profile.numRollers);
405 for (ShellySettingsRoller roller : status.rollers) {
406 if (roller.isValid) {
407 ShellyControlRoller control = api.getRollerStatus(i);
408 Integer relayIndex = i + 1;
409 String groupName = profile.numRollers > 1 ? CHANNEL_GROUP_ROL_CONTROL + relayIndex.toString()
410 : CHANNEL_GROUP_ROL_CONTROL;
412 createRollerChannels(control);
414 if (control.name != null) {
415 updated |= updateChannel(groupName, CHANNEL_OUTPUT_NAME, getStringType(control.name));
418 String state = getString(control.state);
419 if (state.equals(SHELLY_ALWD_ROLLER_TURN_STOP)) { // only valid in stop state
420 int pos = Math.max(SHELLY_MIN_ROLLER_POS, Math.min(control.currentPos, SHELLY_MAX_ROLLER_POS));
421 logger.debug("{}: REST Update roller position: control={}, position={}", thingName,
422 SHELLY_MAX_ROLLER_POS - pos, pos);
423 updated |= updateChannel(groupName, CHANNEL_ROL_CONTROL_CONTROL,
424 toQuantityType((double) (SHELLY_MAX_ROLLER_POS - pos), Units.PERCENT));
425 updated |= updateChannel(groupName, CHANNEL_ROL_CONTROL_POS,
426 toQuantityType((double) pos, Units.PERCENT));
427 scheduledUpdates = 1; // one more poll and then stop
430 updated |= updateChannel(groupName, CHANNEL_ROL_CONTROL_STATE, new StringType(state));
431 updated |= updateChannel(groupName, CHANNEL_ROL_CONTROL_STOPR, getStringType(control.stopReason));
432 updated |= updateChannel(groupName, CHANNEL_ROL_CONTROL_SAFETY, getOnOff(control.safetySwitch));
442 * Update Relay/Roller channels
444 * @param th Thing Handler instance
445 * @param profile ShellyDeviceProfile
446 * @param status Last ShellySettingsStatus
448 * @throws ShellyApiException
450 public boolean updateDimmers(ShellySettingsStatus orgStatus) throws ShellyApiException {
451 boolean updated = false;
452 if (profile.isDimmer) {
453 // We need to fixup the returned Json: The dimmer returns light[] element, which is ok, but it doesn't have
454 // the same structure as lights[] from Bulb,RGBW2 and Duo. The tag gets replaced by dimmers[] so that Gson
455 // maps to a different structure (ShellyShortLight).
456 Gson gson = new Gson();
457 ShellySettingsStatus dstatus = fromJson(gson, ShellyApiJsonDTO.fixDimmerJson(orgStatus.json),
458 ShellySettingsStatus.class);
460 logger.trace("{}: Updating {} dimmers(s)", thingName, dstatus.dimmers.size());
462 for (ShellyShortLightStatus dimmer : dstatus.dimmers) {
464 String groupName = profile.numRelays <= 1 ? CHANNEL_GROUP_DIMMER_CONTROL
465 : CHANNEL_GROUP_DIMMER_CONTROL + r.toString();
467 createDimmerChannels(dstatus, l);
469 // On a status update we map a dimmer.ison = false to brightness 0 rather than the device's brightness
470 // and send a OFF status to the same channel.
471 // When the device's brightness is > 0 we send the new value to the channel and a ON command
473 updated |= updateChannel(groupName, CHANNEL_BRIGHTNESS + "$Switch", OnOffType.ON);
474 updated |= updateChannel(groupName, CHANNEL_BRIGHTNESS + "$Value",
475 toQuantityType((double) getInteger(dimmer.brightness), DIGITS_NONE, Units.PERCENT));
477 updated |= updateChannel(groupName, CHANNEL_BRIGHTNESS + "$Switch", OnOffType.OFF);
478 updated |= updateChannel(groupName, CHANNEL_BRIGHTNESS + "$Value",
479 toQuantityType(0.0, DIGITS_NONE, Units.PERCENT));
482 ShellySettingsDimmer dsettings = profile.settings.dimmers.get(l);
483 if (dsettings != null) {
484 updated |= updateChannel(groupName, CHANNEL_TIMER_AUTOON,
485 toQuantityType(getDouble(dsettings.autoOn), Units.SECOND));
486 updated |= updateChannel(groupName, CHANNEL_TIMER_AUTOOFF,
487 toQuantityType(getDouble(dsettings.autoOff), Units.SECOND));
497 * Update LED channels
499 * @param th Thing Handler instance
500 * @param profile ShellyDeviceProfile
501 * @param status Last ShellySettingsStatus
503 public boolean updateLed(ShellySettingsStatus status) {
504 boolean updated = false;
505 updated |= updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_LED_STATUS_DISABLE,
506 getOnOff(profile.settings.ledStatusDisable));
507 updated |= updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_LED_POWER_DISABLE,
508 getOnOff(profile.settings.ledPowerDisable));