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.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;
49 import com.google.gson.Gson;
52 * The{@link ShellyRelayHandler} handles light (bulb+rgbw2) specific commands and status. All other commands will be
53 * handled by the generic thing handler.
55 * @author Markus Michels - Initial contribution
58 public class ShellyRelayHandler extends ShellyBaseHandler {
59 private final Logger logger = LoggerFactory.getLogger(ShellyRelayHandler.class);
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
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);
77 public void initialize() {
82 public boolean handleDeviceCommand(ChannelUID channelUID, Command command) throws ShellyApiException {
84 String groupName = getString(channelUID.getGroupId());
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;
94 switch (channelUID.getIdWithoutGroup()) {
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);
104 logger.debug("{}: Device is in roller mode, channel command {} ignored", thingName, channelUID);
107 case CHANNEL_BRIGHTNESS: // e.g.Dimmer, Duo
108 handleBrightness(command, rIndex);
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));
117 // request updates the next 45sec to update roller position after it stopped
118 requestUpdates(autoCoIoT ? 1 : 45 / UPDATE_STATUS_INTERVAL_SECONDS, false);
121 case CHANNEL_TIMER_AUTOON:
122 logger.debug("{}: Set Auto-ON timer to {}", thingName, command);
123 api.setTimer(rIndex, SHELLY_TIMER_AUTOON, getNumber(command));
125 case CHANNEL_TIMER_AUTOOFF:
126 logger.debug("{}: Set Auto-OFF timer to {}", thingName, command);
127 api.setTimer(rIndex, SHELLY_TIMER_AUTOOFF, getNumber(command));
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
141 * @throws ShellyApiException
143 private void handleBrightness(Command command, Integer index) throws ShellyApiException {
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);
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);
158 value = Math.max(light.brightness - DIM_STEPSIZE, 0);
160 logger.debug("{}: Increase/Decrease brightness from {} to {}", thingName, light.brightness, value);
162 validateRange("brightness", value, 0, 100);
164 // Switch light off on brightness = 0
166 logger.debug("{}: Brightness=0 -> switch output OFF", thingName);
167 updateBrightnessChannel(index, OnOffType.OFF, 0);
169 logger.debug("{}: Setting dimmer brightness to {}", thingName, value);
170 updateBrightnessChannel(index, OnOffType.ON, value);
174 private void updateBrightnessChannel(int lightId, OnOffType power, int brightness) throws ShellyApiException {
175 if (brightness > 0) {
176 api.setBrightness(lightId, brightness, config.brightnessAutoOn);
178 api.setRelayTurn(lightId, power == OnOffType.ON ? SHELLY_API_ON : SHELLY_API_OFF);
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));
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);
196 * Handle Roller Commands
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
204 private void handleRoller(Command command, String groupName, Integer index, boolean isControl)
205 throws ShellyApiException {
206 Integer position = -1;
208 if ((command instanceof UpDownType) || (command instanceof OnOffType)) {
209 ShellyControlRoller rstatus = api.getRollerStatus(index);
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);
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;
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;
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);
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();
248 throw new IllegalArgumentException(
249 "Invalid value type for roller control/posiution" + command.getClass().toString());
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);
258 logger.debug("{}: Changing roller position to {}", thingName, position);
259 api.setRollerPos(index, position);
261 if (position != -1) {
262 // make sure both are in sync
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));
267 updateChannel(groupName, CHANNEL_ROL_CONTROL_POS, new PercentType(position));
273 * Auto-create relay channels depending on relay type/mode
275 private void createRelayChannels(ShellyStatusRelay relay, int idx) {
276 if (!areChannelsCreated()) {
277 updateChannelDefinitions(ShellyChannelDefinitionsDTO.createRelayChannels(getThing(), profile, relay, idx));
281 private void createRollerChannels(ShellyControlRoller roller) {
282 if (!areChannelsCreated()) {
283 updateChannelDefinitions(ShellyChannelDefinitionsDTO.createRollerChannels(getThing(), roller));
288 * Update Relay/Roller channels
290 * @param th Thing Handler instance
291 * @param profile ShellyDeviceProfile
292 * @param status Last ShellySettingsStatus
294 * @throws ShellyApiException
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);
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));
311 if (getBool(relay.overpower)) {
312 postEvent(ALARM_TYPE_OVERPOWER, false);
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));
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));
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));
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));
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));
347 // Update input(s) state
348 updated |= updateInputs(groupName, status, i);
354 // Check for Relay in Roller Mode
355 if (profile.hasRelays && profile.isRoller && (status.rollers != null)) {
356 logger.trace("{}: Updating {} rollers", thingName, profile.numRollers);
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;
366 createRollerChannels(control);
368 if (control.name != null) {
369 updated |= updateChannel(groupName, CHANNEL_OUTPUT_NAME, getStringType(control.name));
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
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);
394 * Update Relay/Roller channels
396 * @param th Thing Handler instance
397 * @param profile ShellyDeviceProfile
398 * @param status Last ShellySettingsStatus
400 * @throws ShellyApiException
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);
412 logger.trace("{}: Updating {} dimmers(s)", thingName, dstatus.dimmers.size());
414 for (ShellyShortLightStatus dimmer : dstatus.dimmers) {
416 String groupName = profile.numRelays <= 1 ? CHANNEL_GROUP_DIMMER_CONTROL
417 : CHANNEL_GROUP_DIMMER_CONTROL + r.toString();
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
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));
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));
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));
440 updated |= updateInputs(groupName, orgStatus, l);
448 * Update LED channels
450 * @param th Thing Handler instance
451 * @param profile ShellyDeviceProfile
452 * @param status Last ShellySettingsStatus
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));