2 * Copyright (c) 2010-2022 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.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));
353 if (profile.hasRelays && !profile.isRoller && !profile.isDimmer) {
354 logger.trace("{}: Updating {} relay(s)", thingName, profile.numRelays);
356 ShellyStatusRelay rstatus = api.getRelayStatus(i);
357 for (ShellyShortStatusRelay relay : rstatus.relays) {
358 createRelayChannels(rstatus, i);
359 if ((relay.isValid == null) || relay.isValid) {
360 String groupName = profile.getControlGroup(i);
361 ShellySettingsRelay rs = profile.settings.relays.get(i);
362 updated |= updateChannel(groupName, CHANNEL_OUTPUT_NAME, getStringType(rs.name));
364 if (getBool(relay.overpower)) {
365 postEvent(ALARM_TYPE_OVERPOWER, false);
368 updated |= updateChannel(groupName, CHANNEL_OUTPUT, getOnOff(relay.ison));
369 updated |= updateChannel(groupName, CHANNEL_TIMER_ACTIVE, getOnOff(relay.hasTimer));
370 if (rstatus.extTemperature != null) {
371 // Shelly 1/1PM support up to 3 external sensors
372 // for whatever reason those are not represented as an array, but 3 elements
373 if (rstatus.extTemperature.sensor1 != null) {
374 updated |= updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_ESENDOR_TEMP1, toQuantityType(
375 getDouble(rstatus.extTemperature.sensor1.tC), DIGITS_TEMP, SIUnits.CELSIUS));
377 if (rstatus.extTemperature.sensor2 != null) {
378 updated |= updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_ESENDOR_TEMP2, toQuantityType(
379 getDouble(rstatus.extTemperature.sensor2.tC), DIGITS_TEMP, SIUnits.CELSIUS));
381 if (rstatus.extTemperature.sensor3 != null) {
382 updated |= updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_ESENDOR_TEMP3, toQuantityType(
383 getDouble(rstatus.extTemperature.sensor3.tC), DIGITS_TEMP, SIUnits.CELSIUS));
386 if ((rstatus.extHumidity != null) && (rstatus.extHumidity.sensor1 != null)) {
387 updated |= updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_HUM, toQuantityType(
388 getDouble(rstatus.extHumidity.sensor1.hum), DIGITS_PERCENT, Units.PERCENT));
391 // Update Auto-ON/OFF timer
392 ShellySettingsRelay rsettings = profile.settings.relays.get(i);
393 if (rsettings != null) {
394 updated |= updateChannel(groupName, CHANNEL_TIMER_AUTOON,
395 toQuantityType(getDouble(rsettings.autoOn), Units.SECOND));
396 updated |= updateChannel(groupName, CHANNEL_TIMER_AUTOOFF,
397 toQuantityType(getDouble(rsettings.autoOff), Units.SECOND));
402 } else if (profile.hasRelays && profile.isRoller && (status.rollers != null)) {
403 // Check for Relay in Roller Mode
404 logger.trace("{}: Updating {} rollers", thingName, profile.numRollers);
407 for (ShellySettingsRoller roller : status.rollers) {
408 if (roller.isValid) {
409 ShellyControlRoller control = api.getRollerStatus(i);
410 Integer relayIndex = i + 1;
411 String groupName = profile.numRollers > 1 ? CHANNEL_GROUP_ROL_CONTROL + relayIndex.toString()
412 : CHANNEL_GROUP_ROL_CONTROL;
414 createRollerChannels(control);
416 if (control.name != null) {
417 updated |= updateChannel(groupName, CHANNEL_OUTPUT_NAME, getStringType(control.name));
420 String state = getString(control.state);
421 if (state.equals(SHELLY_ALWD_ROLLER_TURN_STOP)) { // only valid in stop state
422 int pos = Math.max(SHELLY_MIN_ROLLER_POS, Math.min(control.currentPos, SHELLY_MAX_ROLLER_POS));
423 logger.debug("{}: REST Update roller position: control={}, position={}", thingName,
424 SHELLY_MAX_ROLLER_POS - pos, pos);
425 updated |= updateChannel(groupName, CHANNEL_ROL_CONTROL_CONTROL,
426 toQuantityType((double) (SHELLY_MAX_ROLLER_POS - pos), Units.PERCENT));
427 updated |= updateChannel(groupName, CHANNEL_ROL_CONTROL_POS,
428 toQuantityType((double) pos, Units.PERCENT));
429 scheduledUpdates = 1; // one more poll and then stop
432 updated |= updateChannel(groupName, CHANNEL_ROL_CONTROL_STATE, new StringType(state));
433 updated |= updateChannel(groupName, CHANNEL_ROL_CONTROL_STOPR, getStringType(control.stopReason));
434 updated |= updateChannel(groupName, CHANNEL_ROL_CONTROL_SAFETY, getOnOff(control.safetySwitch));
444 * Update Relay/Roller channels
446 * @param th Thing Handler instance
447 * @param profile ShellyDeviceProfile
448 * @param status Last ShellySettingsStatus
450 * @throws ShellyApiException
452 public boolean updateDimmers(ShellySettingsStatus orgStatus) throws ShellyApiException {
453 boolean updated = false;
454 if (profile.isDimmer) {
455 // We need to fixup the returned Json: The dimmer returns light[] element, which is ok, but it doesn't have
456 // the same structure as lights[] from Bulb,RGBW2 and Duo. The tag gets replaced by dimmers[] so that Gson
457 // maps to a different structure (ShellyShortLight).
458 Gson gson = new Gson();
459 ShellySettingsStatus dstatus = fromJson(gson, ShellyApiJsonDTO.fixDimmerJson(orgStatus.json),
460 ShellySettingsStatus.class);
462 logger.trace("{}: Updating {} dimmers(s)", thingName, dstatus.dimmers.size());
464 for (ShellyShortLightStatus dimmer : dstatus.dimmers) {
466 String groupName = profile.numRelays <= 1 ? CHANNEL_GROUP_DIMMER_CONTROL
467 : CHANNEL_GROUP_DIMMER_CONTROL + r.toString();
469 createDimmerChannels(dstatus, l);
471 // On a status update we map a dimmer.ison = false to brightness 0 rather than the device's brightness
472 // and send a OFF status to the same channel.
473 // When the device's brightness is > 0 we send the new value to the channel and a ON command
475 updated |= updateChannel(groupName, CHANNEL_BRIGHTNESS + "$Switch", OnOffType.ON);
476 updated |= updateChannel(groupName, CHANNEL_BRIGHTNESS + "$Value",
477 toQuantityType((double) getInteger(dimmer.brightness), DIGITS_NONE, Units.PERCENT));
479 updated |= updateChannel(groupName, CHANNEL_BRIGHTNESS + "$Switch", OnOffType.OFF);
480 updated |= updateChannel(groupName, CHANNEL_BRIGHTNESS + "$Value",
481 toQuantityType(0.0, DIGITS_NONE, Units.PERCENT));
484 if (profile.settings.dimmers != null) {
485 ShellySettingsDimmer dsettings = profile.settings.dimmers.get(l);
486 if (dsettings != null) {
487 updated |= updateChannel(groupName, CHANNEL_TIMER_AUTOON,
488 toQuantityType(getDouble(dsettings.autoOn), Units.SECOND));
489 updated |= updateChannel(groupName, CHANNEL_TIMER_AUTOOFF,
490 toQuantityType(getDouble(dsettings.autoOff), Units.SECOND));
501 * Update LED channels
503 * @param th Thing Handler instance
504 * @param profile ShellyDeviceProfile
505 * @param status Last ShellySettingsStatus
507 public boolean updateLed(ShellySettingsStatus status) {
508 boolean updated = false;
509 updated |= updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_LED_STATUS_DISABLE,
510 getOnOff(profile.settings.ledStatusDisable));
511 updated |= updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_LED_POWER_DISABLE,
512 getOnOff(profile.settings.ledPowerDisable));