diff --git a/bundles/org.openhab.binding.insteon/README.md b/bundles/org.openhab.binding.insteon/README.md index 07e724a70d5..a9fe1aca19f 100644 --- a/bundles/org.openhab.binding.insteon/README.md +++ b/bundles/org.openhab.binding.insteon/README.md @@ -298,8 +298,9 @@ In order to determine which channels a device supports, check the device in the | program-lock | Switch | R/W | Local Programming Lock | | pump | Switch | R/W | Pump Control | | ramp-rate | Number:Time | R/W | Ramp Rate | -| relay-mode | String | R/W | Output Relay Mode | -| relay-sensor-follow | Switch | R/W | Output Relay Sensor Follow | +| relay-mode | String | R/W | Relay Mode | +| relay-sensor-follow | Switch | R/W | Relay Sensor Follow | +| relay-sensor-inverted | Switch | R/W | Relay Sensor Inverted | | resume-dim | Switch | R/W | Resume Dim Level | | reverse-direction | Switch | R/W | Reverse Motor Direction | | rollershutter | Rollershutter | R/W | Rollershutter | @@ -1122,21 +1123,33 @@ OFF=unlocked ### I/O Linc (garage door openers) -The I/O Linc devices are really two devices in one: a relay and a contact. +The I/O Linc devices are really two devices in one: an output relay and an input contact sensor. To control the relay, link the modem as a controller using the set buttons as described in the instructions. -To get the status of the contact, the modem must also be linked as a responder to the I/O Linc. -The I/O Linc has a feature to invert the contact or match the contact when it sends commands to any linked responders. -This is based on the status of the contact when it is linked, and was intended for controlling other devices with the contact. -The binding expects the contact to be inverted to work properly. -Ensure the contact is OFF (status LED is dark/garage door open) when linking the modem as a responder to the I/O Linc in order for it to function properly. +To get the state of the relay and sensor, the modem must also be linked as a responder to the I/O Linc. +The contact state is based on the sensor state at the time it is linked. +To invert the state, either relink the modem as a responder with the sensor state inverted, or toggle the channel `relay-sensor-inverted`. +By default, the device is inverted where an on command is sent when the sensor is closed, and off when open. +For a garage door opener, ensure the input sensor is closed (status LED off) during the linking process. ##### Items ```java -Switch garageDoorOpener "door opener" { channel="insteon:device:home:aabbcc:switch" } -Contact garageDoorContact "door contact [MAP(contact.map):%s]" { channel="insteon:device:home:aabbcc:contact" } +Switch garageDoorOpener "door opener" { channel="insteon:device:home:aabbcc:switch" } +Contact garageDoorContact "door contact [MAP(contact.map):%s]" { channel="insteon:device:home:aabbcc:contact" } +String garageDoorRelayMode "door relay mode" { channel="insteon:device:home:aabbcc:relay-mode" } +Switch garageDoorRelaySensorInverted "door relay sensor inverted" { channel="insteon:device:home:aabbcc:relay-sensor-inverted" } ``` +
+ Legacy + + ```java + Switch garageDoorOpener "door opener" { channel="insteon:device:home:AABBCC:switch" } + Contact garageDoorContact "door contact [MAP(contact.map):%s]" { channel="insteon:device:home:AABBCC:contact" } + ``` + +
+ and create a file "contact.map" in the transforms directory with these entries: ```text @@ -1145,11 +1158,6 @@ CLOSED=closed -=unknown ``` -> NOTE: If the I/O Linc contact status appears delayed, or returns the wrong value when the sensor changes states, the contact was likely ON (status LED lit) when the modem was linked as a responder. -Examples of this behavior would include: The status remaining CLOSED for up to 3 minutes after the door is opened, or the status remains OPEN for up to three minutes after the garage is opened and immediately closed again. -To resolve this behavior the I/O Linc will need to be unlinked and then re-linked to the modem with the contact OFF (stats LED off). -That would be with the door open when using the Insteon garage kit. - ### Fan Controllers Here is an example configuration for a FanLinc module, which has a dimmable light and a variable speed fan: diff --git a/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/InsteonBindingConstants.java b/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/InsteonBindingConstants.java index 79ca1c7fcaf..84bd1b5d847 100644 --- a/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/InsteonBindingConstants.java +++ b/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/InsteonBindingConstants.java @@ -75,10 +75,13 @@ public class InsteonBindingConstants { public static final String FEATURE_LED_ON_OFF = "ledOnOff"; public static final String FEATURE_LINK_FF_GROUP = "linkFFGroup"; public static final String FEATURE_LOW_BATTERY_THRESHOLD = "lowBatteryThreshold"; + public static final String FEATURE_MOMENTARY_DURATION = "momentaryDuration"; public static final String FEATURE_MONITOR_MODE = "monitorMode"; public static final String FEATURE_ON_LEVEL = "onLevel"; public static final String FEATURE_PING = "ping"; public static final String FEATURE_RAMP_RATE = "rampRate"; + public static final String FEATURE_RELAY_MODE = "relayMode"; + public static final String FEATURE_RELAY_SENSOR_FOLLOW = "relaySensorFollow"; public static final String FEATURE_SCENE = "scene"; public static final String FEATURE_STAY_AWAKE = "stayAwake"; public static final String FEATURE_TEMPERATURE_SCALE = "temperatureScale"; diff --git a/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/device/DeviceFeature.java b/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/device/DeviceFeature.java index 16462551372..53993bd80b0 100644 --- a/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/device/DeviceFeature.java +++ b/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/device/DeviceFeature.java @@ -237,9 +237,9 @@ public class DeviceFeature { public int getComponentId() { int componentId = 0; if (device instanceof InsteonDevice insteonDevice && isControllerOrResponderFeature()) { - // use feature group as component id if device has more than one controller or responder feature, + // use feature group as component id if device has more than one controller or responder feature group, // set to 1 if device link db has a matching record, otherwise fall back to 0 - if (insteonDevice.getControllerOrResponderFeatures().size() > 1) { + if (insteonDevice.getControllerOrResponderFeatureGroups().size() > 1) { componentId = getGroup(); } else if (insteonDevice.getLinkDB().hasComponentIdRecord(1, isControllerFeature())) { componentId = 1; diff --git a/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/device/InsteonDevice.java b/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/device/InsteonDevice.java index 8d1b2c25209..bc7446f57c4 100644 --- a/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/device/InsteonDevice.java +++ b/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/device/InsteonDevice.java @@ -112,8 +112,9 @@ public class InsteonDevice extends BaseDevice getControllerOrResponderFeatures() { - return getFeatures().stream().filter(DeviceFeature::isControllerOrResponderFeature).toList(); + public List getControllerOrResponderFeatureGroups() { + return getFeatures().stream().filter(DeviceFeature::isControllerOrResponderFeature).map(DeviceFeature::getGroup) + .distinct().toList(); } public List getFeatures(String type) { diff --git a/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/device/feature/CommandHandler.java b/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/device/feature/CommandHandler.java index 7a6fea91660..dfb22ee07dc 100644 --- a/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/device/feature/CommandHandler.java +++ b/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/device/feature/CommandHandler.java @@ -1727,6 +1727,10 @@ public abstract class CommandHandler extends BaseFeatureHandler { * I/O linc momentary duration command handler */ public static class IOLincMomentaryDurationCommandHandler extends CommandHandler { + private static final double DEFAULT_DURATION = 2; + private static final double MIN_DURATION = 0.1; + private static final double MAX_DURATION = 6300; + IOLincMomentaryDurationCommandHandler(DeviceFeature feature) { super(feature); } @@ -1740,34 +1744,26 @@ public abstract class CommandHandler extends BaseFeatureHandler { public void handleCommand(InsteonChannelConfiguration config, Command cmd) { try { double duration = getDuration(cmd); - if (duration != -1) { - InsteonAddress address = getInsteonDevice().getAddress(); - int prescaler = 1; - int delay = (int) Math.round(duration * 10); - if (delay > 255) { - prescaler = (int) Math.ceil(delay / 255.0); - delay = (int) Math.round(delay / (double) prescaler); - } - boolean setCRC = getInsteonDevice().getInsteonEngine().supportsChecksum(); - // define ext command message to set momentary duration delay - Msg delayMsg = Msg.makeExtendedMessage(address, (byte) 0x2E, (byte) 0x00, - new byte[] { (byte) 0x01, (byte) 0x06, (byte) delay }, setCRC); - // define ext command message to set momentary duration prescaler - Msg prescalerMsg = Msg.makeExtendedMessage(address, (byte) 0x2E, (byte) 0x00, - new byte[] { (byte) 0x01, (byte) 0x07, (byte) prescaler }, setCRC); - // send requests - feature.sendRequest(delayMsg); - if (logger.isDebugEnabled()) { - logger.debug("{}: sent momentary duration delay {} request to {}", nm(), - HexUtils.getHexString(delay), address); - } - feature.sendRequest(prescalerMsg); - if (logger.isDebugEnabled()) { - logger.debug("{}: sent momentary duration prescaler {} request to {}", nm(), - HexUtils.getHexString(prescaler), address); - } - } else { - logger.warn("{}: got unexpected momentary duration command {}, ignoring request", nm(), cmd); + int multiplier = getDurationMultiplier(duration); + int delay = (int) Math.round(duration * 10 / multiplier); + InsteonAddress address = getInsteonDevice().getAddress(); + boolean setCRC = getInsteonDevice().getInsteonEngine().supportsChecksum(); + // define ext command message to set momentary duration delay + Msg delayMsg = Msg.makeExtendedMessage(address, (byte) 0x2E, (byte) 0x00, + new byte[] { (byte) 0x01, (byte) 0x06, (byte) delay }, setCRC); + // define ext command message to set momentary duration multiplier + Msg multiplierMsg = Msg.makeExtendedMessage(address, (byte) 0x2E, (byte) 0x00, + new byte[] { (byte) 0x01, (byte) 0x07, (byte) multiplier }, setCRC); + // send requests + feature.sendRequest(delayMsg); + if (logger.isDebugEnabled()) { + logger.debug("{}: sent momentary duration delay {} request to {}", nm(), + HexUtils.getHexString(delay), address); + } + feature.sendRequest(multiplierMsg); + if (logger.isDebugEnabled()) { + logger.debug("{}: sent momentary duration multiplier {} request to {}", nm(), + HexUtils.getHexString(multiplier), address); } } catch (InvalidMessageTypeException e) { logger.warn("{}: invalid message: ", nm(), e); @@ -1777,12 +1773,29 @@ public abstract class CommandHandler extends BaseFeatureHandler { } private double getDuration(Command cmd) { + double duration = DEFAULT_DURATION; if (cmd instanceof DecimalType time) { - return time.doubleValue(); + duration = time.doubleValue(); } else if (cmd instanceof QuantityType time) { - return Objects.requireNonNullElse(time.toInvertibleUnit(Units.SECOND), time).doubleValue(); + duration = Objects.requireNonNullElse(time.toInvertibleUnit(Units.SECOND), time).doubleValue(); } - return -1; + return Math.max(MIN_DURATION, Math.min(MAX_DURATION, duration)); + } + + private int getDurationMultiplier(double duration) { + int delay = (int) Math.round(duration * 10); + // multiplier discrete steps as used by the insteon app + int multiplier = 1; + if (delay > 51000) { + multiplier = 250; + } else if (delay > 25500) { + multiplier = 200; + } else if (delay > 2550) { + multiplier = 100; + } else if (delay > 255) { + multiplier = 10; + } + return multiplier; } } diff --git a/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/device/feature/MessageHandler.java b/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/device/feature/MessageHandler.java index f12dc8d130b..0368fbabba2 100644 --- a/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/device/feature/MessageHandler.java +++ b/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/device/feature/MessageHandler.java @@ -19,6 +19,7 @@ import java.math.BigDecimal; import java.math.RoundingMode; import java.time.Instant; import java.util.Map; +import java.util.Objects; import java.util.Set; import javax.measure.Unit; @@ -1265,6 +1266,83 @@ public abstract class MessageHandler extends BaseFeatureHandler { } } + /** + * I/O linc relay switch on message handler + */ + public static class IOLincRelaySwitchOnMsgHandler extends SwitchOnMsgHandler { + IOLincRelaySwitchOnMsgHandler(DeviceFeature feature) { + super(feature); + } + + @Override + public void handleMessage(byte cmd1, Msg msg) { + State state = getInsteonDevice().getFeatureState(FEATURE_RELAY_SENSOR_FOLLOW); + // handle message only if relay sensor follow is on + if (OnOffType.ON.equals(state)) { + super.handleMessage(cmd1, msg); + } + } + } + + /** + * I/O linc relay switch off message handler + */ + public static class IOLincRelaySwitchOffMsgHandler extends SwitchOffMsgHandler { + IOLincRelaySwitchOffMsgHandler(DeviceFeature feature) { + super(feature); + } + + @Override + public void handleMessage(byte cmd1, Msg msg) { + State state = getInsteonDevice().getFeatureState(FEATURE_RELAY_SENSOR_FOLLOW); + // handle message only if relay sensor follow is on + if (OnOffType.ON.equals(state)) { + super.handleMessage(cmd1, msg); + } + } + } + + /** + * I/O linc relay switch reply message handler + */ + public static class IOLincRelaySwitchReplyHandler extends SwitchRequestReplyHandler { + private static final int DEFAULT_DURATION = 2; + + IOLincRelaySwitchReplyHandler(DeviceFeature feature) { + super(feature); + } + + @Override + public void handleMessage(byte cmd1, Msg msg) { + super.handleMessage(cmd1, msg); + // trigger poll with delay based on momentary duration if not status reply and relay mode not latching + if (feature.getQueryCommand() != 0x19 && getRelayMode() != IOLincRelayMode.LATCHING) { + long delay = getPollDelay(); + feature.triggerPoll(delay); + } + } + + private @Nullable IOLincRelayMode getRelayMode() { + try { + State state = getInsteonDevice().getFeatureState(FEATURE_RELAY_MODE); + if (state instanceof StringType mode) { + return IOLincRelayMode.valueOf(mode.toString()); + } + } catch (IllegalArgumentException ignored) { + } + return null; + } + + private long getPollDelay() { + double delay = DEFAULT_DURATION; + State state = getInsteonDevice().getFeatureState(FEATURE_MOMENTARY_DURATION); + if (state instanceof QuantityType duration) { + delay = Objects.requireNonNullElse(duration.toInvertibleUnit(Units.SECOND), duration).doubleValue(); + } + return (long) (delay * 1000); + } + } + /** * I/O linc momentary duration message handler */ @@ -1275,17 +1353,17 @@ public abstract class MessageHandler extends BaseFeatureHandler { @Override protected @Nullable State getState(byte cmd1, double value) { - int duration = getDuration((int) value); + double duration = getDuration((int) value); return new QuantityType