2 * Copyright (c) 2010-2020 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 * PaperUI Control has a combined Slider for Brightness combined with On/Off
135 * Brightness channel has 2 functions: Switch On/Off (OnOnType) and setting brightness (PercentType)
136 * There is some more logic in the control. When brightness is set to 0 the control sends also an OFF command
137 * When current brightness is 0 and slider will be moved the new brightness will be set, but also a ON command is
142 * @throws ShellyApiException
144 private void handleBrightness(Command command, Integer index) throws ShellyApiException {
146 if (command instanceof PercentType) { // Dimmer
147 value = ((PercentType) command).intValue();
148 } else if (command instanceof DecimalType) { // Number
149 value = ((DecimalType) command).intValue();
150 } else if (command instanceof OnOffType) { // Switch
151 logger.debug("{}: Switch output {}", thingName, command);
152 updateBrightnessChannel(index, (OnOffType) command, value);
154 } else if (command instanceof IncreaseDecreaseType) {
155 ShellyShortLightStatus light = api.getLightStatus(index);
156 if (((IncreaseDecreaseType) command).equals(IncreaseDecreaseType.INCREASE)) {
157 value = Math.min(light.brightness + DIM_STEPSIZE, 100);
159 value = Math.max(light.brightness - DIM_STEPSIZE, 0);
161 logger.debug("{}: Increase/Decrease brightness from {} to {}", thingName, light.brightness, value);
163 validateRange("brightness", value, 0, 100);
165 // Switch light off on brightness = 0
167 logger.debug("{}: Brightness=0 -> switch output OFF", thingName);
168 updateBrightnessChannel(index, OnOffType.OFF, 0);
170 logger.debug("{}: Setting dimmer brightness to {}", thingName, value);
171 updateBrightnessChannel(index, OnOffType.ON, value);
175 private void updateBrightnessChannel(int lightId, OnOffType power, int brightness) throws ShellyApiException {
176 if (brightness > 0) {
177 api.setBrightness(lightId, brightness, config.brightnessAutoOn);
179 api.setRelayTurn(lightId, power == OnOffType.ON ? SHELLY_API_ON : SHELLY_API_OFF);
181 updateChannel(CHANNEL_COLOR_WHITE, CHANNEL_BRIGHTNESS + "$Switch", power);
182 updateChannel(CHANNEL_COLOR_WHITE, CHANNEL_BRIGHTNESS + "$Value",
183 toQuantityType(new Double(power == OnOffType.ON ? brightness : 0), DIGITS_NONE, Units.PERCENT));
187 public boolean updateDeviceStatus(ShellySettingsStatus status) throws ShellyApiException {
188 // map status to channels
189 boolean updated = false;
190 updated |= updateRelays(status);
191 updated |= updateDimmers(status);
192 updated |= updateLed(status);
197 * Handle Roller Commands
199 * @param command from handleCommand()
200 * @param groupName relay, roller...
201 * @param index relay number
202 * @param isControl true: is the Rollershutter channel, false: rollerpos channel
203 * @throws ShellyApiException
205 private void handleRoller(Command command, String groupName, Integer index, boolean isControl)
206 throws ShellyApiException {
207 Integer position = -1;
209 if ((command instanceof UpDownType) || (command instanceof OnOffType)) {
210 ShellyControlRoller rstatus = api.getRollerStatus(index);
212 if (!getString(rstatus.state).isEmpty() && !getString(rstatus.state).equals(SHELLY_ALWD_ROLLER_TURN_STOP)) {
213 boolean up = command instanceof UpDownType && (UpDownType) command == UpDownType.UP;
214 boolean down = command instanceof UpDownType && (UpDownType) command == UpDownType.DOWN;
215 if ((up && getString(rstatus.state).equals(SHELLY_ALWD_ROLLER_TURN_OPEN))
216 || (down && getString(rstatus.state).equals(SHELLY_ALWD_ROLLER_TURN_CLOSE))) {
217 logger.debug("{}: Roller is already moving ({}), ignore command {}", thingName,
218 getString(rstatus.state), command);
219 requestUpdates(1, false);
224 if (((command instanceof UpDownType) && UpDownType.UP.equals(command))
225 || ((command instanceof OnOffType) && OnOffType.ON.equals(command))) {
226 logger.debug("{}: Open roller", thingName);
227 api.setRollerTurn(index, SHELLY_ALWD_ROLLER_TURN_OPEN);
228 position = SHELLY_MAX_ROLLER_POS;
231 if (((command instanceof UpDownType) && UpDownType.DOWN.equals(command))
232 || ((command instanceof OnOffType) && OnOffType.OFF.equals(command))) {
233 logger.debug("{}: Closing roller", thingName);
234 api.setRollerTurn(index, SHELLY_ALWD_ROLLER_TURN_CLOSE);
235 position = SHELLY_MIN_ROLLER_POS;
237 } else if ((command instanceof StopMoveType) && StopMoveType.STOP.equals(command)) {
238 logger.debug("{}: Stop roller", thingName);
239 api.setRollerTurn(index, SHELLY_ALWD_ROLLER_TURN_STOP);
241 logger.debug("{}: Set roller to position {}", thingName, command);
242 if (command instanceof PercentType) {
243 PercentType p = (PercentType) command;
244 position = p.intValue();
245 } else if (command instanceof DecimalType) {
246 DecimalType d = (DecimalType) command;
247 position = d.intValue();
249 throw new IllegalArgumentException(
250 "Invalid value type for roller control/posiution" + command.getClass().toString());
253 // take position from RollerShutter control and map to Shelly positon (OH:
254 // 0=closed, 100=open; Shelly 0=open, 100=closed)
255 // take position 1:1 from position channel
256 position = isControl ? SHELLY_MAX_ROLLER_POS - position : position;
257 validateRange("roller position", position, SHELLY_MIN_ROLLER_POS, SHELLY_MAX_ROLLER_POS);
259 logger.debug("{}: Changing roller position to {}", thingName, position);
260 api.setRollerPos(index, position);
262 if (position != -1) {
263 // make sure both are in sync
265 int pos = SHELLY_MAX_ROLLER_POS - Math.max(0, Math.min(position, SHELLY_MAX_ROLLER_POS));
266 updateChannel(groupName, CHANNEL_ROL_CONTROL_CONTROL, new PercentType(pos));
268 updateChannel(groupName, CHANNEL_ROL_CONTROL_POS, new PercentType(position));
274 * Auto-create relay channels depending on relay type/mode
276 private void createRelayChannels(ShellyStatusRelay relay, int idx) {
277 if (!areChannelsCreated()) {
278 updateChannelDefinitions(ShellyChannelDefinitionsDTO.createRelayChannels(getThing(), profile, relay, idx));
282 private void createRollerChannels(ShellyControlRoller roller) {
283 if (!areChannelsCreated()) {
284 updateChannelDefinitions(ShellyChannelDefinitionsDTO.createRollerChannels(getThing(), roller));
289 * Update Relay/Roller channels
291 * @param th Thing Handler instance
292 * @param profile ShellyDeviceProfile
293 * @param status Last ShellySettingsStatus
295 * @throws ShellyApiException
297 public boolean updateRelays(ShellySettingsStatus status) throws ShellyApiException {
298 boolean updated = false;
299 // Check for Relay in Standard Mode
300 if (profile.hasRelays && !profile.isRoller && !profile.isDimmer) {
301 logger.trace("{}: Updating {} relay(s)", thingName, profile.numRelays);
304 ShellyStatusRelay rstatus = api.getRelayStatus(i);
305 for (ShellyShortStatusRelay relay : rstatus.relays) {
306 createRelayChannels(rstatus, i);
307 if ((relay.isValid == null) || relay.isValid) {
308 String groupName = profile.getControlGroup(i);
309 ShellySettingsRelay rs = profile.settings.relays.get(i);
310 updated |= updateChannel(groupName, CHANNEL_OUTPUT_NAME, getStringType(rs.name));
312 if (getBool(relay.overpower)) {
313 postEvent(ALARM_TYPE_OVERPOWER, false);
316 updated |= updateChannel(groupName, CHANNEL_OUTPUT, getOnOff(relay.ison));
317 updated |= updateChannel(groupName, CHANNEL_TIMER_ACTIVE, getOnOff(relay.hasTimer));
318 if (rstatus.extTemperature != null) {
319 // Shelly 1/1PM support up to 3 external sensors
320 // for whatever reason those are not represented as an array, but 3 elements
321 if (rstatus.extTemperature.sensor1 != null) {
322 updated |= updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_ESENDOR_TEMP1, toQuantityType(
323 getDouble(rstatus.extTemperature.sensor1.tC), DIGITS_TEMP, SIUnits.CELSIUS));
325 if (rstatus.extTemperature.sensor2 != null) {
326 updated |= updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_ESENDOR_TEMP2, toQuantityType(
327 getDouble(rstatus.extTemperature.sensor2.tC), DIGITS_TEMP, SIUnits.CELSIUS));
329 if (rstatus.extTemperature.sensor3 != null) {
330 updated |= updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_ESENDOR_TEMP3, toQuantityType(
331 getDouble(rstatus.extTemperature.sensor3.tC), DIGITS_TEMP, SIUnits.CELSIUS));
334 if ((rstatus.extHumidity != null) && (rstatus.extHumidity.sensor1 != null)) {
335 updated |= updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_HUM, toQuantityType(
336 getDouble(rstatus.extHumidity.sensor1.hum), DIGITS_PERCENT, Units.PERCENT));
339 // Update Auto-ON/OFF timer
340 ShellySettingsRelay rsettings = profile.settings.relays.get(i);
341 if (rsettings != null) {
342 updated |= updateChannel(groupName, CHANNEL_TIMER_AUTOON,
343 toQuantityType(getDouble(rsettings.autoOn), Units.SECOND));
344 updated |= updateChannel(groupName, CHANNEL_TIMER_AUTOOFF,
345 toQuantityType(getDouble(rsettings.autoOff), Units.SECOND));
348 // Update input(s) state
349 updated |= updateInputs(groupName, status, i);
355 // Check for Relay in Roller Mode
356 if (profile.hasRelays && profile.isRoller && (status.rollers != null)) {
357 logger.trace("{}: Updating {} rollers", thingName, profile.numRollers);
360 for (ShellySettingsRoller roller : status.rollers) {
361 if (roller.isValid) {
362 ShellyControlRoller control = api.getRollerStatus(i);
363 Integer relayIndex = i + 1;
364 String groupName = profile.numRollers > 1 ? CHANNEL_GROUP_ROL_CONTROL + relayIndex.toString()
365 : CHANNEL_GROUP_ROL_CONTROL;
367 createRollerChannels(control);
369 if (control.name != null) {
370 updated |= updateChannel(groupName, CHANNEL_OUTPUT_NAME, getStringType(control.name));
373 String state = getString(control.state);
374 if (state.equals(SHELLY_ALWD_ROLLER_TURN_STOP)) { // only valid in stop state
375 int pos = Math.max(SHELLY_MIN_ROLLER_POS, Math.min(control.currentPos, SHELLY_MAX_ROLLER_POS));
376 updated |= updateChannel(groupName, CHANNEL_ROL_CONTROL_CONTROL,
377 toQuantityType(new Double(SHELLY_MAX_ROLLER_POS - pos), Units.PERCENT));
378 updated |= updateChannel(groupName, CHANNEL_ROL_CONTROL_POS,
379 toQuantityType(new Double(pos), Units.PERCENT));
380 scheduledUpdates = 1; // one more poll and then stop
383 updated |= updateChannel(groupName, CHANNEL_ROL_CONTROL_STATE, new StringType(state));
384 updated |= updateChannel(groupName, CHANNEL_ROL_CONTROL_STOPR, getStringType(control.stopReason));
385 updated |= updateInputs(groupName, status, i);
395 * Update Relay/Roller channels
397 * @param th Thing Handler instance
398 * @param profile ShellyDeviceProfile
399 * @param status Last ShellySettingsStatus
401 * @throws ShellyApiException
403 public boolean updateDimmers(ShellySettingsStatus orgStatus) throws ShellyApiException {
404 boolean updated = false;
405 if (profile.isDimmer) {
406 // We need to fixup the returned Json: The dimmer returns light[] element, which is ok, but it doesn't have
407 // the same structure as lights[] from Bulb,RGBW2 and Duo. The tag gets replaced by dimmers[] so that Gson
408 // maps to a different structure (ShellyShortLight).
409 Gson gson = new Gson();
410 ShellySettingsStatus dstatus = gson.fromJson(ShellyApiJsonDTO.fixDimmerJson(orgStatus.json),
411 ShellySettingsStatus.class);
413 logger.trace("{}: Updating {} dimmers(s)", thingName, dstatus.dimmers.size());
415 for (ShellyShortLightStatus dimmer : dstatus.dimmers) {
417 String groupName = profile.numRelays <= 1 ? CHANNEL_GROUP_DIMMER_CONTROL
418 : CHANNEL_GROUP_DIMMER_CONTROL + r.toString();
420 // On a status update we map a dimmer.ison = false to brightness 0 rather than the device's brightness
421 // and send a OFF status to the same channel.
422 // When the device's brightness is > 0 we send the new value to the channel and a ON command
424 updated |= updateChannel(groupName, CHANNEL_BRIGHTNESS + "$Switch", OnOffType.ON);
425 updated |= updateChannel(groupName, CHANNEL_BRIGHTNESS + "$Value",
426 toQuantityType(new Double(getInteger(dimmer.brightness)), DIGITS_NONE, Units.PERCENT));
428 updated |= updateChannel(groupName, CHANNEL_BRIGHTNESS + "$Switch", OnOffType.OFF);
429 updated |= updateChannel(groupName, CHANNEL_BRIGHTNESS + "$Value",
430 toQuantityType(new Double(0), DIGITS_NONE, Units.PERCENT));
433 ShellySettingsDimmer dsettings = profile.settings.dimmers.get(l);
434 if (dsettings != null) {
435 updated |= updateChannel(groupName, CHANNEL_TIMER_AUTOON,
436 toQuantityType(getDouble(dsettings.autoOn), Units.SECOND));
437 updated |= updateChannel(groupName, CHANNEL_TIMER_AUTOOFF,
438 toQuantityType(getDouble(dsettings.autoOff), Units.SECOND));
441 updated |= updateInputs(groupName, orgStatus, l);
449 * Update LED channels
451 * @param th Thing Handler instance
452 * @param profile ShellyDeviceProfile
453 * @param status Last ShellySettingsStatus
455 public boolean updateLed(ShellySettingsStatus status) {
456 boolean updated = false;
457 updated |= updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_LED_STATUS_DISABLE,
458 getOnOff(profile.settings.ledStatusDisable));
459 updated |= updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_LED_POWER_DISABLE,
460 getOnOff(profile.settings.ledPowerDisable));