[insteon] Fix iolinc device support (#18273)

Signed-off-by: Jeremy Setton <jeremy.setton@gmail.com>
pull/18686/head
Jeremy 2025-06-03 16:06:06 -04:00 committed by GitHub
parent 1f7183b7fd
commit c646a0704a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 187 additions and 64 deletions

View File

@ -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" }
```
<details>
<summary>Legacy</summary>
```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" }
```
</details>
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:

View File

@ -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";

View File

@ -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;

View File

@ -112,8 +112,9 @@ public class InsteonDevice extends BaseDevice<InsteonAddress, InsteonDeviceHandl
return getFeatures().stream().filter(DeviceFeature::isResponderFeature).toList();
}
public List<DeviceFeature> getControllerOrResponderFeatures() {
return getFeatures().stream().filter(DeviceFeature::isControllerOrResponderFeature).toList();
public List<Integer> getControllerOrResponderFeatureGroups() {
return getFeatures().stream().filter(DeviceFeature::isControllerOrResponderFeature).map(DeviceFeature::getGroup)
.distinct().toList();
}
public List<DeviceFeature> getFeatures(String type) {

View File

@ -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;
}
}

View File

@ -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<Time>(duration, Units.SECOND);
}
private int getDuration(int value) {
int prescaler = value >> 8; // high byte
private double getDuration(int value) {
int multiplier = Math.max(value >> 8, 1); // high byte
int delay = value & 0xFF; // low byte
if (delay == 0) {
delay = 255;
}
return delay * prescaler / 10;
return delay * multiplier / 10.0;
}
}

View File

@ -287,13 +287,15 @@ channel-type.insteon.program-lock.description = Prevent manual device configurat
channel-type.insteon.pump.label = Pump Control
channel-type.insteon.ramp-rate.label = Ramp Rate
channel-type.insteon.ramp-rate.description = Control how fast the dimmer turns on or off.
channel-type.insteon.relay-mode.label = Output Relay Mode
channel-type.insteon.relay-mode.label = Relay Mode
channel-type.insteon.relay-mode.state.option.LATCHING = Latching
channel-type.insteon.relay-mode.state.option.MOMENTARY_A = Momentary A
channel-type.insteon.relay-mode.state.option.MOMENTARY_B = Momentary B
channel-type.insteon.relay-mode.state.option.MOMENTARY_C = Momentary C
channel-type.insteon.relay-sensor-follow.label = Output Relay Sensor Follow
channel-type.insteon.relay-sensor-follow.description = Set the output relay to trigger when the sensor input changes state.
channel-type.insteon.relay-sensor-follow.label = Relay Sensor Follow
channel-type.insteon.relay-sensor-follow.description = Set the relay to trigger when the sensor changes state.
channel-type.insteon.relay-sensor-inverted.label = Relay Sensor Inverted
channel-type.insteon.relay-sensor-inverted.description = Invert the sensor state when the relay triggers.
channel-type.insteon.resume-dim.label = Resume Dim Level
channel-type.insteon.resume-dim.description = Resume previous dim level when turned on locally.
channel-type.insteon.reverse-direction.label = Reverse Motor Direction

View File

@ -408,7 +408,7 @@
<item-type>Number:Time</item-type>
<label>Momentary Duration</label>
<description>Set the output relay closure duration for momentary relay modes (A-C).</description>
<state min="0.2" max="25" step="0.1" pattern="%.1f %unit%"/>
<state min="0.1" max="6300" step="0.1" pattern="%.1f %unit%"/>
</channel-type>
<channel-type id="monitor-mode" advanced="true">
@ -539,7 +539,7 @@
<channel-type id="relay-mode" advanced="true">
<item-type>String</item-type>
<label>Output Relay Mode</label>
<label>Relay Mode</label>
<state>
<options>
<option value="LATCHING">Latching</option>
@ -552,8 +552,14 @@
<channel-type id="relay-sensor-follow" advanced="true">
<item-type>Switch</item-type>
<label>Output Relay Sensor Follow</label>
<description>Set the output relay to trigger when the sensor input changes state.</description>
<label>Relay Sensor Follow</label>
<description>Set the relay to trigger when the sensor changes state.</description>
</channel-type>
<channel-type id="relay-sensor-inverted" advanced="true">
<item-type>Switch</item-type>
<label>Relay Sensor Inverted</label>
<description>Invert the sensor state when the relay triggers.</description>
</channel-type>
<channel-type id="resume-dim" advanced="true">

View File

@ -230,6 +230,16 @@
<poll-handler>NoPollHandler</poll-handler>
</feature-type>
<feature-type name="IOLincRelaySwitch" link="responder">
<message-dispatcher>DefaultDispatcher</message-dispatcher>
<message-handler command="0x11" group="1">IOLincRelaySwitchOnMsgHandler</message-handler>
<message-handler command="0x13" group="1">IOLincRelaySwitchOffMsgHandler</message-handler>
<message-handler command="0x19">IOLincRelaySwitchReplyHandler</message-handler>
<message-handler default="true">NoOpMsgHandler</message-handler>
<command-handler command="OnOffType">SwitchOnOffCommandHandler</command-handler>
<command-handler command="RefreshType">RefreshCommandHandler</command-handler>
<poll-handler ext="0" cmd1="0x19" cmd2="0x00">FlexPollHandler</poll-handler>
</feature-type>
<feature-type name="IOLincRelayMode">
<message-dispatcher>DefaultDispatcher</message-dispatcher>
<message-handler command="0x19">IOLincRelayModeReplyHandler</message-handler>
@ -1102,5 +1112,6 @@
<message-handler command="0x16" group="1">X10EventMsgHandler</message-handler>
<message-handler default="true">NoOpMsgHandler</message-handler>
<command-handler default="true">NoOpCommandHandler</command-handler>
<poll-handler>NoPollHandler</poll-handler>
</feature-type>
</xml>

View File

@ -825,8 +825,8 @@
<!-- Sensors and Actuators -->
<device-type name="SensorsActuators_IOLinc">
<feature name="switch">GenericSwitch</feature>
<feature name="contact" group="2">GenericSensorContact</feature>
<feature name="switch">IOLincRelaySwitch</feature>
<feature name="contact">GenericSensorContact</feature>
<feature-group name="extDataGroup" type="ExtDataGroup">
<feature name="momentaryDuration">IOLincMomentaryDuration</feature>
</feature-group>
@ -834,9 +834,10 @@
<feature name="programLock" bit="0" on="0x00" off="0x01">OpFlags</feature>
<feature name="ledTraffic" bit="1" on="0x02" off="0x03">OpFlags</feature>
<feature name="relaySensorFollow" bit="2" on="0x04" off="0x05">OpFlags</feature>
<feature name="relaySensorInverted" bit="6" on="0x0E" off="0x0F" inverted="true">OpFlags</feature>
<feature name="relayMode">IOLincRelayMode</feature>
</feature-group>
<default-link name="relay" type="controller" group="1" data1="0x03" data2="0x00" data3="0x00"/>
<default-link name="sensor" type="controller" group="1" data1="0x03" data2="0x00" data3="0x00"/>
</device-type>
<device-type name="SensorsActuators_Siren">