From 0c37a36e5cf60feb68fabc3c9a62521f10ab01c6 Mon Sep 17 00:00:00 2001 From: Bob A Date: Wed, 29 Jul 2020 08:24:33 -0400 Subject: [PATCH] [lutron] Add setLevel thing action to dimmer (#8153) * [lutron] Workaround for thing actions bug * [lutron] Fix NPE * Fix NPE when setting fadeInTime and fadeOutTime variables prior to handler initialization Also-by: Austin Guiswite Signed-off-by: Bob Adair --- bundles/org.openhab.binding.lutron/README.md | 33 ++++- .../binding/lutron/action/DimmerActions.java | 130 ++++++++++++++++ .../binding/lutron/action/IDimmerActions.java | 31 ++++ .../internal/handler/DimmerHandler.java | 24 ++- .../internal/protocol/LutronDuration.java | 140 ++++++++++++++++++ 5 files changed, 353 insertions(+), 5 deletions(-) create mode 100644 bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/action/DimmerActions.java create mode 100644 bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/action/IDimmerActions.java create mode 100644 bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/protocol/LutronDuration.java diff --git a/bundles/org.openhab.binding.lutron/README.md b/bundles/org.openhab.binding.lutron/README.md index 9087f415d9236..d367a282ccc1e 100644 --- a/bundles/org.openhab.binding.lutron/README.md +++ b/bundles/org.openhab.binding.lutron/README.md @@ -104,7 +104,10 @@ Bridge lutron:ipbridge:radiora2 [ ipAddress="192.168.1.2", user="lutron", passwo ### Dimmers -Dimmers can optionally be configured to specify a fade in and fade out time in seconds using the `fadeInTime` and `fadeOutTime` parameters. +Dimmers can optionally be configured to specify a default fade in and fade out time in seconds using the `fadeInTime` and `fadeOutTime` parameters. +These are used for ON and OFF commands, respectively, and default to 1 second if not set. +Commands using a specific percent value will use a default fade time of 0.25 seconds. + A **dimmer** thing has a single channel *lightlevel* with type Dimmer and category DimmableLight. Thing configuration file example: @@ -113,6 +116,19 @@ Thing configuration file example: Thing dimmer livingroom [ integrationId=8, fadeInTime=0.5, fadeOutTime=5 ] ``` +The **dimmer** thing supports the thing action `setLevel(Double level, Double fadeTime, Double delayTime)` for automation rules. + +The parameters are: + +* `level` The new light level to set (0-100) +* `fadeTime` The time in seconds over which the dimmer should fade to the new level +* `delayTime` The time in seconds to delay before starting to fade to the new level + +The fadeTime and delayTime parameters are significant to 2 digits after the decimal point (i.e. to hundredths of a second), but some Lutron systems may round the time to the nearest 0.25 seconds when processing the command. +Times of 100 seconds or more will be rounded to the nearest integer value. + +See below for an example rule using thing actions. + ### Switches Switches take no additional parameters besides `integrationId`. @@ -594,7 +610,7 @@ The only exceptions are **greenmode** *step*, which is periodically polled and a Many other channels accept REFRESH commands to initiate a poll, but sending one should not normally be necessary. -## RadioRA 2 Configuration File Example +## RadioRA 2/HomeWorks QS Configuration File Examples: demo.things: @@ -634,6 +650,19 @@ Rollershutter Lib_Shade1 "Shade 1" { channel="lutron:shade:radiora2: ``` +dimmerAction.rules: + +``` +rule "Test dimmer action" +when + Item TestSwitch received command ON +then + val actions = getActions("lutron","lutron:dimmer:radiora2:lrtable") + actions.setLevel(100, 5.5, 0) +end +``` + + # Lutron RadioRA (Classic) Binding This binding integrates with the legacy Lutron RadioRA (Classic) lighting system. diff --git a/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/action/DimmerActions.java b/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/action/DimmerActions.java new file mode 100644 index 0000000000000..455f60a2ef5de --- /dev/null +++ b/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/action/DimmerActions.java @@ -0,0 +1,130 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.lutron.action; + +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.math.BigDecimal; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.smarthome.core.thing.binding.ThingActions; +import org.eclipse.smarthome.core.thing.binding.ThingActionsScope; +import org.eclipse.smarthome.core.thing.binding.ThingHandler; +import org.openhab.binding.lutron.internal.handler.DimmerHandler; +import org.openhab.binding.lutron.internal.protocol.LutronDuration; +import org.openhab.core.automation.annotation.ActionInput; +import org.openhab.core.automation.annotation.RuleAction; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link DimmerActions} defines thing actions for DimmerHandler. + * + * @author Bob Adair - Initial contribution + */ +@ThingActionsScope(name = "lutron") +@NonNullByDefault +public class DimmerActions implements ThingActions, IDimmerActions { + private final Logger logger = LoggerFactory.getLogger(DimmerActions.class); + + private @Nullable DimmerHandler handler; + + public DimmerActions() { + logger.trace("Lutron Dimmer actions service created"); + } + + @Override + public void setThingHandler(@Nullable ThingHandler handler) { + if (handler instanceof DimmerHandler) { + this.handler = (DimmerHandler) handler; + } + } + + @Override + public @Nullable ThingHandler getThingHandler() { + return handler; + } + + /** + * The setLevel dimmer thing action + */ + @Override + @RuleAction(label = "setLevel", description = "Send set level command with fade and delay times") + public void setLevel( + @ActionInput(name = "level", label = "Dimmer Level", description = "New dimmer level (0-100)") @Nullable Double level, + @ActionInput(name = "fadeTime", label = "Fade Time", description = "Time to fade to new level (seconds)") @Nullable Double fadeTime, + @ActionInput(name = "delayTime", label = "Delay Time", description = "Delay before starting fade (seconds)") @Nullable Double delayTime) { + DimmerHandler dimmerHandler = handler; + if (dimmerHandler == null) { + logger.debug("Handler not set for Dimmer thing actions."); + return; + } + if (level == null) { + logger.debug("Ignoring setLevel command due to null level value."); + return; + } + if (fadeTime == null) { + logger.debug("Ignoring setLevel command due to null value for fadeTime."); + return; + } + if (delayTime == null) { + logger.debug("Ignoring setLevel command due to null value for delayTime."); + return; + } + + Double lightLevel = level; + if (lightLevel > 100.0) { + lightLevel = 100.0; + } else if (lightLevel < 0.0) { + lightLevel = 0.0; + } + try { + dimmerHandler.setLightLevel(new BigDecimal(lightLevel).setScale(2, BigDecimal.ROUND_HALF_UP), + new LutronDuration(fadeTime), new LutronDuration(delayTime)); + } catch (IllegalArgumentException e) { + logger.debug("Ignoring setLevel command due to illegal argument exception: {}", e.getMessage()); + } + } + + /** + * Static setLevel method for Rules DSL backward compatibility + */ + public static void setLevel(@Nullable ThingActions actions, @Nullable Double level, @Nullable Double fadeTime, + @Nullable Double delayTime) { + invokeMethodOf(actions).setLevel(level, fadeTime, delayTime); // Replace when core issue #1536 is fixed + } + + /** + * This is only necessary to work around a bug in openhab-core (issue #1536). It should be removed once that is + * resolved. + */ + private static IDimmerActions invokeMethodOf(@Nullable ThingActions actions) { + if (actions == null) { + throw new IllegalArgumentException("actions cannot be null"); + } + if (actions.getClass().getName().equals(DimmerActions.class.getName())) { + if (actions instanceof IDimmerActions) { + return (IDimmerActions) actions; + } else { + return (IDimmerActions) Proxy.newProxyInstance(IDimmerActions.class.getClassLoader(), + new Class[] { IDimmerActions.class }, (Object proxy, Method method, Object[] args) -> { + Method m = actions.getClass().getDeclaredMethod(method.getName(), + method.getParameterTypes()); + return m.invoke(actions, args); + }); + } + } + throw new IllegalArgumentException("Actions is not an instance of DimmerActions"); + } +} diff --git a/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/action/IDimmerActions.java b/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/action/IDimmerActions.java new file mode 100644 index 0000000000000..9e3ad90c609bd --- /dev/null +++ b/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/action/IDimmerActions.java @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.lutron.action; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * The {@link IDimmerActions} interface defines the interface for all thing actions supported by the dimmer thing. + * This is only necessary to work around a bug in openhab-core (issue #1536). It should be removed once that is + * resolved. + * + * @author Bob Adair - Initial contribution + * + */ +@NonNullByDefault +public interface IDimmerActions { + + public void setLevel(@Nullable Double level, @Nullable Double fadeTime, @Nullable Double delayTime); + +} diff --git a/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/handler/DimmerHandler.java b/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/handler/DimmerHandler.java index 37cfd3ead29f9..938aa32fe1896 100644 --- a/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/handler/DimmerHandler.java +++ b/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/handler/DimmerHandler.java @@ -15,6 +15,8 @@ import static org.openhab.binding.lutron.internal.LutronBindingConstants.CHANNEL_LIGHTLEVEL; import java.math.BigDecimal; +import java.util.Collection; +import java.util.Collections; import org.eclipse.smarthome.core.library.types.OnOffType; import org.eclipse.smarthome.core.library.types.PercentType; @@ -23,9 +25,12 @@ import org.eclipse.smarthome.core.thing.Thing; import org.eclipse.smarthome.core.thing.ThingStatus; import org.eclipse.smarthome.core.thing.ThingStatusDetail; +import org.eclipse.smarthome.core.thing.binding.ThingHandlerService; import org.eclipse.smarthome.core.types.Command; +import org.openhab.binding.lutron.action.DimmerActions; import org.openhab.binding.lutron.internal.config.DimmerConfig; import org.openhab.binding.lutron.internal.protocol.LutronCommandType; +import org.openhab.binding.lutron.internal.protocol.LutronDuration; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -41,11 +46,18 @@ public class DimmerHandler extends LutronHandler { private final Logger logger = LoggerFactory.getLogger(DimmerHandler.class); private DimmerConfig config; + private LutronDuration fadeInTime; + private LutronDuration fadeOutTime; public DimmerHandler(Thing thing) { super(thing); } + @Override + public Collection> getServices() { + return Collections.singletonList(DimmerActions.class); + } + @Override public int getIntegrationId() { if (config == null) { @@ -62,6 +74,8 @@ public void initialize() { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "No integrationId configured"); return; } + fadeInTime = new LutronDuration(config.fadeInTime); + fadeOutTime = new LutronDuration(config.fadeOutTime); logger.debug("Initializing Dimmer handler for integration ID {}", getIntegrationId()); initDeviceState(); @@ -94,16 +108,20 @@ public void handleCommand(ChannelUID channelUID, Command command) { if (channelUID.getId().equals(CHANNEL_LIGHTLEVEL)) { if (command instanceof Number) { int level = ((Number) command).intValue(); - output(ACTION_ZONELEVEL, level, 0.25); } else if (command.equals(OnOffType.ON)) { - output(ACTION_ZONELEVEL, 100, this.config.fadeInTime); + output(ACTION_ZONELEVEL, 100, fadeInTime); } else if (command.equals(OnOffType.OFF)) { - output(ACTION_ZONELEVEL, 0, this.config.fadeOutTime); + output(ACTION_ZONELEVEL, 0, fadeOutTime); } } } + public void setLightLevel(BigDecimal level, LutronDuration fade, LutronDuration delay) { + int intLevel = level.intValue(); + output(ACTION_ZONELEVEL, intLevel, fade, delay); + } + @Override public void handleUpdate(LutronCommandType type, String... parameters) { if (type == LutronCommandType.OUTPUT && parameters.length > 1 diff --git a/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/protocol/LutronDuration.java b/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/protocol/LutronDuration.java new file mode 100644 index 0000000000000..ef1c1aded81ca --- /dev/null +++ b/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/protocol/LutronDuration.java @@ -0,0 +1,140 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.lutron.internal.protocol; + +import java.math.BigDecimal; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Holds time durations used by the Lutron protocols + * + * @author Bob Adair - Initial contribution + * + */ +@NonNullByDefault +public class LutronDuration { + public static final int MAX_SECONDS = 360000 - 1; + public static final int MAX_HUNDREDTHS = 99; + + private static final Pattern PATTERN_SS = Pattern.compile("^(\\d{1,2})$"); + private static final Pattern PATTERN_SSDEC = Pattern.compile("^(\\d{1,2})\\.(\\d{2})$"); + private static final Pattern PATTERN_MMSS = Pattern.compile("^(\\d{1,2}):(\\d{2})$"); + private static final Pattern PATTERN_HHMMSS = Pattern.compile("^(\\d{1,2}):(\\d{2}):(\\d{2})$"); + + public final Integer seconds; + public final Integer hundredths; + + /** + * Constructor accepting duration in seconds + */ + public LutronDuration(Integer seconds) { + if (seconds < 0 || seconds > MAX_SECONDS) { + throw new IllegalArgumentException("Invalid duration"); + } + this.seconds = seconds; + this.hundredths = 0; + } + + /** + * Constructor accepting duration in seconds and hundredths of seconds + */ + public LutronDuration(Integer seconds, Integer hundredths) { + if (seconds < 0 || seconds > MAX_SECONDS || hundredths < 0 || hundredths > MAX_HUNDREDTHS) { + throw new IllegalArgumentException("Invalid duration"); + } + this.seconds = seconds; + this.hundredths = hundredths; + } + + /** + * Constructor accepting duration in seconds as a BigDecimal + */ + public LutronDuration(BigDecimal seconds) { + if (seconds.compareTo(BigDecimal.ZERO) == -1 || seconds.compareTo(new BigDecimal(MAX_SECONDS)) == 1) { + new IllegalArgumentException("Invalid duration"); + } + this.seconds = seconds.intValue(); + BigDecimal fractional = seconds.subtract(new BigDecimal(seconds.intValue())); + this.hundredths = fractional.movePointRight(2).intValue(); + } + + /** + * Constructor accepting duration in seconds as a Double + */ + public LutronDuration(Double seconds) { + this(new BigDecimal(seconds).setScale(2, BigDecimal.ROUND_HALF_UP)); + } + + /** + * Constructor accepting duration string of the format: SS.ss, SS, MM:SS, or HH:MM:SS + */ + public LutronDuration(String duration) { + Matcher matcherSS = PATTERN_SS.matcher(duration); + if (matcherSS.find()) { + Integer seconds = Integer.valueOf(matcherSS.group(1)); + this.seconds = seconds; + this.hundredths = 0; + return; + } + Matcher matcherSSDec = PATTERN_SSDEC.matcher(duration); + if (matcherSSDec.find()) { + this.seconds = Integer.valueOf(matcherSSDec.group(1)); + this.hundredths = Integer.valueOf(matcherSSDec.group(2)); + return; + } + Matcher matcherMMSS = PATTERN_MMSS.matcher(duration); + if (matcherMMSS.find()) { + Integer minutes = Integer.valueOf(matcherMMSS.group(1)); + Integer seconds = Integer.valueOf(matcherMMSS.group(2)); + this.seconds = minutes * 60 + seconds; + this.hundredths = 0; + return; + } + Matcher matcherHHMMSS = PATTERN_HHMMSS.matcher(duration); + if (matcherHHMMSS.find()) { + Integer hours = Integer.valueOf(matcherHHMMSS.group(1)); + Integer minutes = Integer.valueOf(matcherHHMMSS.group(2)); + Integer seconds = Integer.valueOf(matcherHHMMSS.group(3)); + this.seconds = hours * 60 * 60 + minutes * 60 + seconds; + this.hundredths = 0; + return; + } + throw new IllegalArgumentException("Invalid duration"); + } + + public String asLipString() { + if (seconds < 100) { + if (hundredths == 0) { + return String.valueOf(seconds); + } else { + return String.format("%d.%02d", seconds, hundredths); + } + } else if (seconds < 3600) { + return String.format("%d:%02d", seconds / 60, seconds % 60); + } else { + return String.format("%d:%02d:%02d", seconds / 3600, (seconds % 3600) / 60, (seconds % 60)); + } + } + + public String asLeapString() { + return ""; // TBD + } + + @Override + public String toString() { + return asLipString(); + } +}