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.api1.Shelly1ApiJsonDTO.*;
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.api1.Shelly1ApiJsonDTO;
23 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyRollerStatus;
24 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsDimmer;
25 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsRelay;
26 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsStatus;
27 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyShortLightStatus;
28 import org.openhab.binding.shelly.internal.api1.Shelly1CoapServer;
29 import org.openhab.binding.shelly.internal.config.ShellyBindingConfiguration;
30 import org.openhab.binding.shelly.internal.provider.ShellyChannelDefinitions;
31 import org.openhab.binding.shelly.internal.provider.ShellyTranslationProvider;
32 import org.openhab.core.library.types.DecimalType;
33 import org.openhab.core.library.types.IncreaseDecreaseType;
34 import org.openhab.core.library.types.OnOffType;
35 import org.openhab.core.library.types.PercentType;
36 import org.openhab.core.library.types.StopMoveType;
37 import org.openhab.core.library.types.UpDownType;
38 import org.openhab.core.library.unit.Units;
39 import org.openhab.core.thing.ChannelUID;
40 import org.openhab.core.thing.Thing;
41 import org.openhab.core.types.Command;
42 import org.slf4j.Logger;
43 import org.slf4j.LoggerFactory;
45 import com.google.gson.Gson;
48 * The{@link ShellyRelayHandler} handles light (bulb+rgbw2) specific commands and status. All other commands will be
49 * handled by the generic thing handler.
51 * @author Markus Michels - Initial contribution
54 public class ShellyRelayHandler extends ShellyBaseHandler {
55 private final Logger logger = LoggerFactory.getLogger(ShellyRelayHandler.class);
60 * @param thing The thing passed by the HandlerFactory
61 * @param bindingConfig configuration of the binding
62 * @param coapServer coap server instance
63 * @param localIP local IP of the openHAB host
64 * @param httpPort port of the openHAB HTTP API
66 public ShellyRelayHandler(final Thing thing, final ShellyTranslationProvider translationProvider,
67 final ShellyBindingConfiguration bindingConfig, final Shelly1CoapServer coapServer, final String localIP,
68 int httpPort, final HttpClient httpClient) {
69 super(thing, translationProvider, bindingConfig, coapServer, localIP, httpPort, httpClient);
73 public void initialize() {
78 public boolean handleDeviceCommand(ChannelUID channelUID, Command command) throws ShellyApiException {
80 String groupName = getString(channelUID.getGroupId());
82 if (groupName.startsWith(CHANNEL_GROUP_RELAY_CONTROL)
83 && groupName.length() > CHANNEL_GROUP_RELAY_CONTROL.length()) {
84 rIndex = Integer.parseInt(substringAfter(channelUID.getGroupId(), CHANNEL_GROUP_RELAY_CONTROL)) - 1;
85 } else if (groupName.startsWith(CHANNEL_GROUP_ROL_CONTROL)
86 && groupName.length() > CHANNEL_GROUP_ROL_CONTROL.length()) {
87 rIndex = Integer.parseInt(substringAfter(channelUID.getGroupId(), CHANNEL_GROUP_ROL_CONTROL)) - 1;
90 switch (channelUID.getIdWithoutGroup()) {
95 if (!profile.isRoller) {
96 // extract relay number of group name (relay0->0, relay1->1...)
97 logger.debug("{}: Set relay output to {}", thingName, command);
98 api.setRelayTurn(rIndex, command == OnOffType.ON ? SHELLY_API_ON : SHELLY_API_OFF);
100 logger.debug("{}: Device is in roller mode, channel command {} ignored", thingName, channelUID);
103 case CHANNEL_BRIGHTNESS: // e.g.Dimmer, Duo
104 handleBrightness(command, rIndex);
107 case CHANNEL_ROL_CONTROL_POS:
108 case CHANNEL_ROL_CONTROL_CONTROL:
109 logger.debug("{}: Roller command/position {}", thingName, command);
110 handleRoller(command, groupName, rIndex,
111 channelUID.getIdWithoutGroup().equals(CHANNEL_ROL_CONTROL_CONTROL));
113 // request updates the next 45sec to update roller position after it stopped
114 requestUpdates(autoCoIoT ? 1 : 45 / UPDATE_STATUS_INTERVAL_SECONDS, false);
117 case CHANNEL_ROL_CONTROL_FAV:
118 if (command instanceof Number) {
119 int id = ((Number) command).intValue() - 1;
120 int pos = profile.getRollerFav(id);
122 logger.debug("{}: Selecting favorite {}, position = {}", thingName, id, pos);
123 api.setRollerPos(rIndex, pos);
127 logger.debug("{}: Invalid favorite index: {}", thingName, command);
130 case CHANNEL_TIMER_AUTOON:
131 logger.debug("{}: Set Auto-ON timer to {}", thingName, command);
132 api.setTimer(rIndex, SHELLY_TIMER_AUTOON, getNumber(command).intValue());
134 case CHANNEL_TIMER_AUTOOFF:
135 logger.debug("{}: Set Auto-OFF timer to {}", thingName, command);
136 api.setTimer(rIndex, SHELLY_TIMER_AUTOOFF, getNumber(command).intValue());
143 * Brightness channel has 2 functions: Switch On/Off (OnOnType) and setting brightness (PercentType)
144 * There is some more logic in the control. When brightness is set to 0 the control sends also an OFF command
145 * When current brightness is 0 and slider will be moved the new brightness will be set, but also a ON command is
150 * @throws ShellyApiException
152 private void handleBrightness(Command command, Integer index) throws ShellyApiException {
154 if (command instanceof PercentType) { // Dimmer
155 value = ((PercentType) command).intValue();
156 } else if (command instanceof DecimalType) { // Number
157 value = ((DecimalType) command).intValue();
158 } else if (command instanceof OnOffType) { // Switch
159 logger.debug("{}: Switch output {}", thingName, command);
160 updateBrightnessChannel(index, (OnOffType) command, value);
162 } else if (command instanceof IncreaseDecreaseType) {
163 ShellyShortLightStatus light = api.getLightStatus(index);
164 if (command == IncreaseDecreaseType.INCREASE) {
165 value = Math.min(light.brightness + DIM_STEPSIZE, 100);
167 value = Math.max(light.brightness - DIM_STEPSIZE, 0);
169 logger.debug("{}: Increase/Decrease brightness from {} to {}", thingName, light.brightness, value);
171 validateRange("brightness", value, 0, 100);
173 // Switch light off on brightness = 0
175 logger.debug("{}: Brightness=0 -> switch output OFF", thingName);
176 updateBrightnessChannel(index, OnOffType.OFF, 0);
178 logger.debug("{}: Setting dimmer brightness to {}", thingName, value);
179 updateBrightnessChannel(index, OnOffType.ON, value);
183 private void updateBrightnessChannel(int lightId, OnOffType power, int brightness) throws ShellyApiException {
184 updateChannel(CHANNEL_COLOR_WHITE, CHANNEL_BRIGHTNESS + "$Switch", power);
185 if (brightness > 0) {
186 api.setBrightness(lightId, brightness, config.brightnessAutoOn);
188 api.setRelayTurn(lightId, power == OnOffType.ON ? SHELLY_API_ON : SHELLY_API_OFF);
189 if (brightness >= 0) { // ignore -1
190 updateChannel(CHANNEL_COLOR_WHITE, CHANNEL_BRIGHTNESS + "$Value",
191 toQuantityType((double) (power == OnOffType.ON ? brightness : 0), DIGITS_NONE, Units.PERCENT));
197 public boolean updateDeviceStatus(ShellySettingsStatus status) throws ShellyApiException {
198 // map status to channels
199 boolean updated = false;
200 updated |= updateRelays(status);
201 updated |= updateDimmers(status);
202 updated |= updateLed(status);
207 * Handle Roller Commands
209 * @param command from handleCommand()
210 * @param groupName relay, roller...
211 * @param index relay number
212 * @param isControl true: is the Rollershutter channel, false: rollerpos channel
213 * @throws ShellyApiException
215 private void handleRoller(Command command, String groupName, Integer index, boolean isControl)
216 throws ShellyApiException {
219 if ((command instanceof UpDownType) || (command instanceof OnOffType)) {
220 ShellyRollerStatus rstatus = api.getRollerStatus(index);
222 if (!getString(rstatus.state).isEmpty() && !getString(rstatus.state).equals(SHELLY_ALWD_ROLLER_TURN_STOP)) {
223 if ((command == UpDownType.UP && getString(rstatus.state).equals(SHELLY_ALWD_ROLLER_TURN_OPEN))
224 || (command == UpDownType.DOWN
225 && getString(rstatus.state).equals(SHELLY_ALWD_ROLLER_TURN_CLOSE))) {
226 logger.debug("{}: Roller is already in requested position ({}), ignore command {}", thingName,
227 getString(rstatus.state), command);
228 requestUpdates(1, false);
233 if (command == UpDownType.UP || command == OnOffType.ON
234 || ((command instanceof DecimalType) && (((DecimalType) command).intValue() == 100))) {
235 logger.debug("{}: Open roller", thingName);
236 int shpos = profile.getRollerFav(config.favoriteUP - 1);
238 logger.debug("{}: Use favoriteUP id {} for positioning roller({}%)", thingName, config.favoriteUP,
240 api.setRollerPos(index, shpos);
243 api.setRollerTurn(index, SHELLY_ALWD_ROLLER_TURN_OPEN);
244 position = SHELLY_MIN_ROLLER_POS;
246 } else if (command == UpDownType.DOWN || command == OnOffType.OFF
247 || ((command instanceof DecimalType) && (((DecimalType) command).intValue() == 0))) {
248 logger.debug("{}: Closing roller", thingName);
249 int shpos = profile.getRollerFav(config.favoriteDOWN - 1);
251 // use favorite position
252 logger.debug("{}: Use favoriteDOWN id {} for positioning roller ({}%)", thingName,
253 config.favoriteDOWN, shpos);
254 api.setRollerPos(index, shpos);
257 api.setRollerTurn(index, SHELLY_ALWD_ROLLER_TURN_CLOSE);
258 position = SHELLY_MAX_ROLLER_POS;
261 } else if (command == StopMoveType.STOP) {
262 logger.debug("{}: Stop roller", thingName);
263 api.setRollerTurn(index, SHELLY_ALWD_ROLLER_TURN_STOP);
265 logger.debug("{}: Set roller to position {}", thingName, command);
266 if (command instanceof PercentType) {
267 PercentType p = (PercentType) command;
268 position = p.intValue();
269 } else if (command instanceof DecimalType) {
270 DecimalType d = (DecimalType) command;
271 position = d.intValue();
273 throw new IllegalArgumentException(
274 "Invalid value type for roller control/position" + command.getClass().toString());
277 // take position from RollerShutter control and map to Shelly positon
278 // OH: 0=closed, 100=open; Shelly 0=open, 100=closed)
279 // take position 1:1 from position channel
280 position = isControl ? SHELLY_MAX_ROLLER_POS - position : position;
281 validateRange("roller position", position, SHELLY_MIN_ROLLER_POS, SHELLY_MAX_ROLLER_POS);
283 logger.debug("{}: Changing roller position to {}", thingName, position);
284 api.setRollerPos(index, position);
287 if (position != -1) {
288 // make sure both are in sync
290 int pos = SHELLY_MAX_ROLLER_POS - Math.max(0, Math.min(position, SHELLY_MAX_ROLLER_POS));
291 logger.debug("{}: Set roller position for control channel to {}", thingName, pos);
292 updateChannel(groupName, CHANNEL_ROL_CONTROL_CONTROL, new PercentType(pos));
294 logger.debug("{}: Set roller position channel to {}", thingName, position);
295 updateChannel(groupName, CHANNEL_ROL_CONTROL_POS, new PercentType(position));
301 * Auto-create relay channels depending on relay type/mode
303 private void createRelayChannels(ShellySettingsRelay relay, int idx) {
304 if (!areChannelsCreated()) {
305 updateChannelDefinitions(ShellyChannelDefinitions.createRelayChannels(getThing(), profile, relay, idx));
309 private void createDimmerChannels(ShellySettingsStatus dstatus, int idx) {
310 if (!areChannelsCreated()) {
311 updateChannelDefinitions(ShellyChannelDefinitions.createDimmerChannels(getThing(), profile, dstatus, idx));
315 private void createRollerChannels(ShellyRollerStatus roller) {
316 if (!areChannelsCreated()) {
317 updateChannelDefinitions(ShellyChannelDefinitions.createRollerChannels(getThing(), roller));
322 * Update Relay/Roller channels
324 * @param th Thing Handler instance
325 * @param profile ShellyDeviceProfile
326 * @param status Last ShellySettingsStatus
328 * @throws ShellyApiException
330 public boolean updateRelays(ShellySettingsStatus status) throws ShellyApiException {
331 boolean updated = false;
332 // Check for Relay in Standard Mode
333 if (profile.hasRelays && !profile.isDimmer) {
335 if (status.voltage == null && profile.settings.supplyVoltage != null) {
336 // Shelly 1PM/1L (fix)
337 voltage = profile.settings.supplyVoltage == 0 ? 110.0 : 220.0;
339 // Shelly 2.5 (measured)
340 voltage = getDouble(status.voltage);
343 updated |= updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_VOLTAGE,
344 toQuantityType(voltage, DIGITS_VOLT, Units.VOLT));
348 if (profile.hasRelays && !profile.isRoller) {
349 logger.trace("{}: Updating {} relay(s)", thingName, profile.numRelays);
350 for (int i = 0; i < status.relays.size(); i++) {
351 createRelayChannels(status.relays.get(i), i);
352 updated |= ShellyComponents.updateRelay(this, status, i);
356 // Check for Relay in Roller Mode
357 logger.trace("{}: Updating {} rollers", thingName, profile.numRollers);
358 for (int i = 0; i < profile.numRollers; i++) {
359 ShellyRollerStatus roller = status.rollers.get(i);
360 createRollerChannels(roller);
361 updated |= ShellyComponents.updateRoller(this, roller, i);
369 * Update Relay/Roller channels
371 * @param th Thing Handler instance
372 * @param profile ShellyDeviceProfile
373 * @param status Last ShellySettingsStatus
375 * @throws ShellyApiException
377 public boolean updateDimmers(ShellySettingsStatus orgStatus) throws ShellyApiException {
378 boolean updated = false;
379 if (profile.isDimmer) {
380 // We need to fixup the returned Json: The dimmer returns light[] element, which is ok, but it doesn't have
381 // the same structure as lights[] from Bulb,RGBW2 and Duo. The tag gets replaced by dimmers[] so that Gson
382 // maps to a different structure (ShellyShortLight).
383 Gson gson = new Gson();
384 ShellySettingsStatus dstatus = fromJson(gson, Shelly1ApiJsonDTO.fixDimmerJson(orgStatus.json),
385 ShellySettingsStatus.class);
387 logger.trace("{}: Updating {} dimmers(s)", thingName, dstatus.dimmers.size());
389 for (ShellyShortLightStatus dimmer : dstatus.dimmers) {
391 String groupName = profile.numRelays <= 1 ? CHANNEL_GROUP_DIMMER_CONTROL
392 : CHANNEL_GROUP_DIMMER_CONTROL + r.toString();
394 createDimmerChannels(dstatus, l);
396 // On a status update we map a dimmer.ison = false to brightness 0 rather than the device's brightness
397 // and send a OFF status to the same channel.
398 // When the device's brightness is > 0 we send the new value to the channel and a ON command
400 updated |= updateChannel(groupName, CHANNEL_BRIGHTNESS + "$Switch", OnOffType.ON);
401 updated |= updateChannel(groupName, CHANNEL_BRIGHTNESS + "$Value",
402 toQuantityType((double) getInteger(dimmer.brightness), DIGITS_NONE, Units.PERCENT));
404 updated |= updateChannel(groupName, CHANNEL_BRIGHTNESS + "$Switch", OnOffType.OFF);
405 updated |= updateChannel(groupName, CHANNEL_BRIGHTNESS + "$Value",
406 toQuantityType(0.0, DIGITS_NONE, Units.PERCENT));
409 if (profile.settings.dimmers != null) {
410 ShellySettingsDimmer dsettings = profile.settings.dimmers.get(l);
411 if (dsettings != null) {
412 updated |= updateChannel(groupName, CHANNEL_TIMER_AUTOON,
413 toQuantityType(getDouble(dsettings.autoOn), Units.SECOND));
414 updated |= updateChannel(groupName, CHANNEL_TIMER_AUTOOFF,
415 toQuantityType(getDouble(dsettings.autoOff), Units.SECOND));
426 * Update LED channels
428 * @param th Thing Handler instance
429 * @param profile ShellyDeviceProfile
430 * @param status Last ShellySettingsStatus
432 public boolean updateLed(ShellySettingsStatus status) {
433 boolean updated = false;
434 updated |= updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_LED_STATUS_DISABLE,
435 getOnOff(profile.settings.ledStatusDisable));
436 updated |= updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_LED_POWER_DISABLE,
437 getOnOff(profile.settings.ledPowerDisable));