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