2 * Copyright (c) 2010-2023 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, ShellyThingTable thingTable,
68 final Shelly1CoapServer coapServer, final HttpClient httpClient) {
69 super(thing, translationProvider, bindingConfig, thingTable, coapServer, 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 if (!autoCoIoT && !profile.isGen2) {
115 requestUpdates(45 / UPDATE_STATUS_INTERVAL_SECONDS, false);
119 case CHANNEL_ROL_CONTROL_FAV:
120 if (command instanceof Number) {
121 int id = ((Number) command).intValue() - 1;
122 int pos = profile.getRollerFav(id);
124 logger.debug("{}: Selecting favorite {}, position = {}", thingName, id, pos);
125 api.setRollerPos(rIndex, pos);
129 logger.debug("{}: Invalid favorite index: {}", thingName, command);
132 case CHANNEL_TIMER_AUTOON:
133 logger.debug("{}: Set Auto-ON timer to {}", thingName, command);
134 api.setAutoTimer(rIndex, SHELLY_TIMER_AUTOON, getNumber(command).doubleValue());
136 case CHANNEL_TIMER_AUTOOFF:
137 logger.debug("{}: Set Auto-OFF timer to {}", thingName, command);
138 api.setAutoTimer(rIndex, SHELLY_TIMER_AUTOOFF, getNumber(command).doubleValue());
140 case CHANNEL_EMETER_RESETTOTAL:
141 logger.debug("{}: Reset Meter Totals", thingName);
142 int mIndex = Integer.parseInt(substringAfter(groupName, CHANNEL_GROUP_METER)) - 1;
143 api.resetMeterTotal(mIndex);
144 updateChannel(groupName, CHANNEL_EMETER_RESETTOTAL, OnOffType.OFF);
151 * Brightness channel has 2 functions: Switch On/Off (OnOnType) and setting brightness (PercentType)
152 * There is some more logic in the control. When brightness is set to 0 the control sends also an OFF command
153 * When current brightness is 0 and slider will be moved the new brightness will be set, but also an ON command is
158 * @throws ShellyApiException
160 private void handleBrightness(Command command, Integer index) throws ShellyApiException {
162 if (command instanceof PercentType) { // Dimmer
163 value = ((PercentType) command).intValue();
164 } else if (command instanceof DecimalType) { // Number
165 value = ((DecimalType) command).intValue();
166 } else if (command instanceof OnOffType) { // Switch
167 logger.debug("{}: Switch output {}", thingName, command);
168 updateBrightnessChannel(index, (OnOffType) command, value);
170 } else if (command instanceof IncreaseDecreaseType) {
171 ShellyShortLightStatus light = api.getLightStatus(index);
172 if (command == IncreaseDecreaseType.INCREASE) {
173 value = Math.min(light.brightness + DIM_STEPSIZE, 100);
175 value = Math.max(light.brightness - DIM_STEPSIZE, 0);
177 logger.debug("{}: Increase/Decrease brightness from {} to {}", thingName, light.brightness, value);
179 validateRange("brightness", value, 0, 100);
181 // Switch light off on brightness = 0
183 logger.debug("{}: Brightness=0 -> switch output OFF", thingName);
184 updateBrightnessChannel(index, OnOffType.OFF, 0);
186 logger.debug("{}: Setting dimmer brightness to {}", thingName, value);
187 updateBrightnessChannel(index, OnOffType.ON, value);
191 private void updateBrightnessChannel(int lightId, OnOffType power, int brightness) throws ShellyApiException {
192 updateChannel(CHANNEL_COLOR_WHITE, CHANNEL_BRIGHTNESS + "$Switch", power);
193 if (brightness > 0) {
194 api.setBrightness(lightId, brightness, config.brightnessAutoOn);
196 api.setRelayTurn(lightId, power == OnOffType.ON ? SHELLY_API_ON : SHELLY_API_OFF);
197 if (brightness >= 0) { // ignore -1
198 updateChannel(CHANNEL_COLOR_WHITE, CHANNEL_BRIGHTNESS + "$Value",
199 toQuantityType((double) (power == OnOffType.ON ? brightness : 0), DIGITS_NONE, Units.PERCENT));
205 public boolean updateDeviceStatus(ShellySettingsStatus status) throws ShellyApiException {
206 // map status to channels
207 boolean updated = false;
208 updated |= updateRelays(status);
209 updated |= updateDimmers(status);
210 updated |= updateLed(status);
215 * Handle Roller Commands
217 * @param command from handleCommand()
218 * @param groupName relay, roller...
219 * @param index relay number
220 * @param isControl true: is the Rollershutter channel, false: rollerpos channel
221 * @throws ShellyApiException
223 private void handleRoller(Command command, String groupName, Integer index, boolean isControl)
224 throws ShellyApiException {
227 if ((command instanceof UpDownType) || (command instanceof OnOffType)) {
228 ShellyRollerStatus rstatus = api.getRollerStatus(index);
230 if (!getString(rstatus.state).isEmpty() && !getString(rstatus.state).equals(SHELLY_ALWD_ROLLER_TURN_STOP)) {
231 if ((command == UpDownType.UP && getString(rstatus.state).equals(SHELLY_ALWD_ROLLER_TURN_OPEN))
232 || (command == UpDownType.DOWN
233 && getString(rstatus.state).equals(SHELLY_ALWD_ROLLER_TURN_CLOSE))) {
234 logger.debug("{}: Roller is already in requested position ({}), ignore command {}", thingName,
235 getString(rstatus.state), command);
236 requestUpdates(1, false);
241 if (command == UpDownType.UP || command == OnOffType.ON
242 || ((command instanceof DecimalType) && (((DecimalType) command).intValue() == 100))) {
243 logger.debug("{}: Open roller", thingName);
244 int shpos = profile.getRollerFav(config.favoriteUP - 1);
246 logger.debug("{}: Use favoriteUP id {} for positioning roller({}%)", thingName, config.favoriteUP,
248 api.setRollerPos(index, shpos);
251 api.setRollerTurn(index, SHELLY_ALWD_ROLLER_TURN_OPEN);
253 } else if (command == UpDownType.DOWN || command == OnOffType.OFF
254 || ((command instanceof DecimalType) && (((DecimalType) command).intValue() == 0))) {
255 logger.debug("{}: Closing roller", thingName);
256 int shpos = profile.getRollerFav(config.favoriteDOWN - 1);
258 // use favorite position
259 logger.debug("{}: Use favoriteDOWN id {} for positioning roller ({}%)", thingName,
260 config.favoriteDOWN, shpos);
261 api.setRollerPos(index, shpos);
264 api.setRollerTurn(index, SHELLY_ALWD_ROLLER_TURN_CLOSE);
267 } else if (command == StopMoveType.STOP) {
268 logger.debug("{}: Stop roller", thingName);
269 api.setRollerTurn(index, SHELLY_ALWD_ROLLER_TURN_STOP);
271 logger.debug("{}: Set roller to position {}", thingName, command);
272 if (command instanceof PercentType) {
273 PercentType p = (PercentType) command;
274 position = p.intValue();
275 } else if (command instanceof DecimalType) {
276 DecimalType d = (DecimalType) command;
277 position = d.intValue();
279 throw new IllegalArgumentException(
280 "Invalid value type for roller control/position" + command.getClass().toString());
283 // take position from RollerShutter control and map to Shelly positon
284 // OH: 0=closed, 100=open; Shelly 0=open, 100=closed)
285 // take position 1:1 from position channel
286 position = isControl ? SHELLY_MAX_ROLLER_POS - position : position;
287 validateRange("roller position", position, SHELLY_MIN_ROLLER_POS, SHELLY_MAX_ROLLER_POS);
289 logger.debug("{}: Changing roller position to {}", thingName, position);
290 api.setRollerPos(index, position);
295 * Auto-create relay channels depending on relay type/mode
297 private void createRelayChannels(ShellySettingsRelay relay, int idx) {
298 if (!areChannelsCreated()) {
299 updateChannelDefinitions(ShellyChannelDefinitions.createRelayChannels(getThing(), profile, relay, idx));
303 private void createDimmerChannels(ShellySettingsStatus dstatus, int idx) {
304 if (!areChannelsCreated()) {
305 updateChannelDefinitions(ShellyChannelDefinitions.createDimmerChannels(getThing(), profile, dstatus, idx));
309 private void createRollerChannels(ShellyRollerStatus roller) {
310 if (!areChannelsCreated()) {
311 updateChannelDefinitions(ShellyChannelDefinitions.createRollerChannels(getThing(), roller));
316 * Update Relay/Roller channels
318 * @param th Thing Handler instance
319 * @param profile ShellyDeviceProfile
320 * @param status Last ShellySettingsStatus
322 * @throws ShellyApiException
324 public boolean updateRelays(ShellySettingsStatus status) throws ShellyApiException {
325 boolean updated = false;
326 if (profile.hasRelays && !profile.isDimmer) {
328 if (status.voltage == null && profile.settings.supplyVoltage != null) {
329 // Shelly 1PM/1L (fix)
330 voltage = profile.settings.supplyVoltage == 0 ? 110.0 : 220.0;
332 // Shelly 2.5 (measured)
333 voltage = getDouble(status.voltage);
336 updated |= updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_VOLTAGE,
337 toQuantityType(voltage, DIGITS_VOLT, Units.VOLT));
340 if (!profile.isRoller) {
341 logger.trace("{}: Updating {} relay(s)", thingName, profile.numRelays);
342 for (int i = 0; i < status.relays.size(); i++) {
343 createRelayChannels(status.relays.get(i), i);
344 updated |= ShellyComponents.updateRelay(this, status, i);
347 // Check for Relay in Roller Mode
348 logger.trace("{}: Updating {} rollers", thingName, profile.numRollers);
349 for (int i = 0; i < profile.numRollers; i++) {
350 ShellyRollerStatus roller = status.rollers.get(i);
351 createRollerChannels(roller);
352 updated |= ShellyComponents.updateRoller(this, roller, i);
360 * Update Relay/Roller channels
362 * @param th Thing Handler instance
363 * @param profile ShellyDeviceProfile
364 * @param status Last ShellySettingsStatus
366 * @throws ShellyApiException
368 public boolean updateDimmers(ShellySettingsStatus orgStatus) throws ShellyApiException {
369 boolean updated = false;
370 if (profile.isDimmer) {
371 // We need to fixup the returned Json: The dimmer returns light[] element, which is ok, but it doesn't have
372 // the same structure as lights[] from Bulb,RGBW2 and Duo. The tag gets replaced by dimmers[] so that Gson
373 // maps to a different structure (ShellyShortLight).
374 Gson gson = new Gson();
375 ShellySettingsStatus dstatus = fromJson(gson, Shelly1ApiJsonDTO.fixDimmerJson(orgStatus.json),
376 ShellySettingsStatus.class);
378 logger.trace("{}: Updating {} dimmers(s)", thingName, dstatus.dimmers.size());
380 for (ShellyShortLightStatus dimmer : dstatus.dimmers) {
382 String groupName = profile.numRelays <= 1 ? CHANNEL_GROUP_DIMMER_CONTROL
383 : CHANNEL_GROUP_DIMMER_CONTROL + r.toString();
385 createDimmerChannels(dstatus, l);
387 // On a status update we map a dimmer.ison = false to brightness 0 rather than the device's brightness
388 // and send an OFF status to the same channel.
389 // When the device's brightness is > 0 we send the new value to the channel and an ON command
391 updated |= updateChannel(groupName, CHANNEL_BRIGHTNESS + "$Switch", OnOffType.ON);
392 updated |= updateChannel(groupName, CHANNEL_BRIGHTNESS + "$Value",
393 toQuantityType((double) getInteger(dimmer.brightness), DIGITS_NONE, Units.PERCENT));
395 updated |= updateChannel(groupName, CHANNEL_BRIGHTNESS + "$Switch", OnOffType.OFF);
396 updated |= updateChannel(groupName, CHANNEL_BRIGHTNESS + "$Value",
397 toQuantityType(0.0, DIGITS_NONE, Units.PERCENT));
400 if (profile.settings.dimmers != null) {
401 ShellySettingsDimmer dsettings = profile.settings.dimmers.get(l);
402 if (dsettings != null) {
403 updated |= updateChannel(groupName, CHANNEL_TIMER_AUTOON,
404 toQuantityType(getDouble(dsettings.autoOn), Units.SECOND));
405 updated |= updateChannel(groupName, CHANNEL_TIMER_AUTOOFF,
406 toQuantityType(getDouble(dsettings.autoOff), Units.SECOND));
417 * Update LED channels
419 * @param th Thing Handler instance
420 * @param profile ShellyDeviceProfile
421 * @param status Last ShellySettingsStatus
423 public boolean updateLed(ShellySettingsStatus status) {
424 boolean updated = false;
425 updated |= updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_LED_STATUS_DISABLE,
426 getOnOff(profile.settings.ledStatusDisable));
427 updated |= updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_LED_POWER_DISABLE,
428 getOnOff(profile.settings.ledPowerDisable));