[insteon] Update remote device support (#17540)

* [insteon] Fix remote device not polled when awake

Signed-off-by: jsetton <jeremy.setton@gmail.com>
Signed-off-by: Ciprian Pascu <contact@ciprianpascu.ro>
This commit is contained in:
Jeremy 2024-10-10 15:02:52 -04:00 committed by Ciprian Pascu
parent 4307003c1c
commit cce5d6d3a5
10 changed files with 426 additions and 224 deletions

View File

@ -13,10 +13,13 @@
package org.openhab.binding.insteon.internal;
import java.io.File;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.insteon.internal.device.feature.FeatureEnums.RemoteSceneButtonConfig;
import org.openhab.binding.insteon.internal.device.feature.FeatureEnums.RemoteSwitchButtonConfig;
import org.openhab.binding.insteon.internal.device.feature.FeatureEnums.VenstarSystemMode;
import org.openhab.core.OpenHAB;
import org.openhab.core.thing.ThingTypeUID;
@ -77,7 +80,6 @@ public class InsteonBindingConstants {
public static final String FEATURE_RAMP_RATE = "rampRate";
public static final String FEATURE_SCENE_ON_OFF = "sceneOnOff";
public static final String FEATURE_STAY_AWAKE = "stayAwake";
public static final String FEATURE_SYSTEM_MODE = "systemMode";
public static final String FEATURE_TEMPERATURE_SCALE = "temperatureScale";
public static final String FEATURE_TWO_GROUPS = "2Groups";
@ -90,6 +92,8 @@ public class InsteonBindingConstants {
public static final String FEATURE_TYPE_KEYPAD_BUTTON_ON_MASK = "KeypadButtonOnMask";
public static final String FEATURE_TYPE_KEYPAD_BUTTON_TOGGLE_MODE = "KeypadButtonToggleMode";
public static final String FEATURE_TYPE_OUTLET_SWITCH = "OutletSwitch";
public static final String FEATURE_TYPE_REMOTE_SCENE_BUTTON_CONFIG = "RemoteSceneButtonConfig";
public static final String FEATURE_TYPE_REMOTE_SWITCH_BUTTON_CONFIG = "RemoteSwitchButtonConfig";
public static final String FEATURE_TYPE_THERMOSTAT_FAN_MODE = "ThermostatFanMode";
public static final String FEATURE_TYPE_THERMOSTAT_SYSTEM_MODE = "ThermostatSystemMode";
public static final String FEATURE_TYPE_THERMOSTAT_COOL_SETPOINT = "ThermostatCoolSetpoint";
@ -99,12 +103,9 @@ public class InsteonBindingConstants {
public static final String FEATURE_TYPE_VENSTAR_COOL_SETPOINT = "VenstarCoolSetpoint";
public static final String FEATURE_TYPE_VENSTAR_HEAT_SETPOINT = "VenstarHeatSetpoint";
// List of specific device types
public static final String DEVICE_TYPE_CLIMATE_CONTROL_VENSTAR_THERMOSTAT = "ClimateControl_VenstarThermostat";
// Map of custom state description options
public static final Map<String, String[]> CUSTOM_STATE_DESCRIPTION_OPTIONS = Map.ofEntries(
// Venstar Thermostat System Mode
Map.entry(DEVICE_TYPE_CLIMATE_CONTROL_VENSTAR_THERMOSTAT + ":" + FEATURE_SYSTEM_MODE,
VenstarSystemMode.names().toArray(String[]::new)));
public static final Map<String, List<String>> CUSTOM_STATE_DESCRIPTION_OPTIONS = Map.ofEntries(
Map.entry(FEATURE_TYPE_REMOTE_SCENE_BUTTON_CONFIG, RemoteSceneButtonConfig.names()),
Map.entry(FEATURE_TYPE_REMOTE_SWITCH_BUTTON_CONFIG, RemoteSwitchButtonConfig.names()),
Map.entry(FEATURE_TYPE_VENSTAR_SYSTEM_MODE, VenstarSystemMode.names()));
}

View File

@ -40,10 +40,11 @@ import org.openhab.binding.insteon.internal.device.database.ModemDB;
import org.openhab.binding.insteon.internal.device.database.ModemDBChange;
import org.openhab.binding.insteon.internal.device.database.ModemDBEntry;
import org.openhab.binding.insteon.internal.device.database.ModemDBRecord;
import org.openhab.binding.insteon.internal.device.feature.FeatureEnums.DeviceTypeRenamer;
import org.openhab.binding.insteon.internal.device.feature.FeatureEnums.KeypadButtonToggleMode;
import org.openhab.binding.insteon.internal.handler.InsteonDeviceHandler;
import org.openhab.binding.insteon.internal.transport.message.FieldException;
import org.openhab.binding.insteon.internal.transport.message.GroupMessageStateMachine;
import org.openhab.binding.insteon.internal.transport.message.GroupMessageStateMachine.GroupMessageType;
import org.openhab.binding.insteon.internal.transport.message.Msg;
import org.openhab.binding.insteon.internal.utils.BinaryUtils;
import org.openhab.core.library.types.DecimalType;
@ -219,49 +220,32 @@ public class InsteonDevice extends BaseDevice<InsteonAddress, InsteonDeviceHandl
}
/**
* Returns if a broadcast message is duplicate
* Returns if an incoming message is duplicate
*
* @param cmd1 the cmd1 from the broadcast message received
* @param timestamp the timestamp from the broadcast message received
* @return true if the broadcast message is duplicate
* @param msg the message received
* @return true if group or broadcast message is duplicate
*/
public boolean isDuplicateBroadcastMsg(byte cmd1, long timestamp) {
synchronized (lastBroadcastReceived) {
long timelapse = timestamp - lastBroadcastReceived.getOrDefault(cmd1, timestamp);
if (timelapse > 0 && timelapse < BCAST_STATE_TIMEOUT) {
return true;
} else {
lastBroadcastReceived.put(cmd1, timestamp);
return false;
}
}
}
/**
* Returns if a group message is duplicate
*
* @param cmd1 cmd1 from the group message received
* @param timestamp the timestamp from the broadcast message received
* @param group the broadcast group
* @param type the group message type that was received
* @return true if the group message is duplicate
*/
public boolean isDuplicateGroupMsg(byte cmd1, long timestamp, int group, GroupMessageType type) {
synchronized (groupState) {
GroupMessageStateMachine stateMachine = groupState.get(group);
if (stateMachine == null) {
stateMachine = new GroupMessageStateMachine();
groupState.put(group, stateMachine);
logger.trace("{} created group {} state", address, group);
}
if (stateMachine.getLastCommand() == cmd1 && stateMachine.getLastTimestamp() == timestamp) {
logger.trace("{} using previous group {} state for {}", address, group, type);
return stateMachine.isDuplicate();
} else {
logger.trace("{} updating group {} state to {}", address, group, type);
return stateMachine.update(address, group, cmd1, timestamp, type);
public boolean isDuplicateMsg(Msg msg) {
try {
if (msg.isAllLinkBroadcastOrCleanup()) {
synchronized (groupState) {
int group = msg.getGroup();
GroupMessageStateMachine stateMachine = groupState.computeIfAbsent(group,
k -> new GroupMessageStateMachine());
return stateMachine != null && stateMachine.isDuplicate(msg);
}
} else if (msg.isBroadcast()) {
synchronized (lastBroadcastReceived) {
byte cmd1 = msg.getByte("command1");
long timestamp = msg.getTimestamp();
Long lastTimestamp = lastBroadcastReceived.put(cmd1, timestamp);
return lastTimestamp != null && Math.abs(timestamp - lastTimestamp) <= BCAST_STATE_TIMEOUT;
}
}
} catch (FieldException e) {
logger.warn("error parsing msg: {}", msg, e);
}
return false;
}
/**
@ -494,6 +478,13 @@ public class InsteonDevice extends BaseDevice<InsteonAddress, InsteonDeviceHandl
getFeatures().stream().filter(DeviceFeature::isStatusFeature)
.forEach(feature -> feature.handleMessage(msg));
}
// poll battery powered device while awake if non-duplicate all link or broadcast message
if ((msg.isAllLinkBroadcastOrCleanup() || msg.isBroadcast()) && isBatteryPowered() && isAwake()
&& !isDuplicateMsg(msg)) {
// add poll delay for non-replayed all link broadcast allowing cleanup msg to be be processed beforehand
long delay = msg.isAllLinkBroadcast() && !msg.isAllLinkSuccessReport() && !msg.isReplayed() ? 1500L : 0L;
doPoll(delay);
}
// notify if responding state changed
if (isPrevResponding != isResponding()) {
statusChanged();
@ -596,12 +587,21 @@ public class InsteonDevice extends BaseDevice<InsteonAddress, InsteonDeviceHandl
}
}
/**
* Updates this device type
*
* @param renamer the device type renamer
*/
public void updateType(DeviceTypeRenamer renamer) {
Optional.ofNullable(getType()).map(DeviceType::getName).map(renamer::getNewDeviceType)
.map(name -> DeviceTypeRegistry.getInstance().getDeviceType(name)).ifPresent(this::updateType);
}
/**
* Updates this device type
*
* @param newType the new device type to use
*/
public void updateType(DeviceType newType) {
ProductData productData = getProductData();
DeviceType currentType = getType();

View File

@ -40,6 +40,8 @@ import org.openhab.binding.insteon.internal.device.feature.FeatureEnums.IOLincRe
import org.openhab.binding.insteon.internal.device.feature.FeatureEnums.KeypadButtonConfig;
import org.openhab.binding.insteon.internal.device.feature.FeatureEnums.KeypadButtonToggleMode;
import org.openhab.binding.insteon.internal.device.feature.FeatureEnums.MicroModuleOpMode;
import org.openhab.binding.insteon.internal.device.feature.FeatureEnums.RemoteSceneButtonConfig;
import org.openhab.binding.insteon.internal.device.feature.FeatureEnums.RemoteSwitchButtonConfig;
import org.openhab.binding.insteon.internal.device.feature.FeatureEnums.SirenAlertType;
import org.openhab.binding.insteon.internal.device.feature.FeatureEnums.ThermostatFanMode;
import org.openhab.binding.insteon.internal.device.feature.FeatureEnums.ThermostatSystemMode;
@ -1154,7 +1156,8 @@ public abstract class CommandHandler extends BaseFeatureHandler {
protected int getOpFlagCommand(Command cmd) {
try {
String config = ((StringType) cmd).toString();
return KeypadButtonConfig.valueOf(config).getValue();
return KeypadButtonConfig.valueOf(config).shouldSetFlag() ? getParameterAsInteger("on", -1)
: getParameterAsInteger("off", -1);
} catch (IllegalArgumentException e) {
logger.warn("{}: got unexpected button config command: {}, ignoring request", nm(), cmd);
return -1;
@ -1845,6 +1848,74 @@ public abstract class CommandHandler extends BaseFeatureHandler {
}
}
/**
* Remote scene button config command handler
*/
public static class RemoteSceneButtonConfigCommandHandler extends MultiOpFlagsCommandHandler {
RemoteSceneButtonConfigCommandHandler(DeviceFeature feature) {
super(feature);
}
@Override
protected Map<Integer, String> getOpFlagCommands(Command cmd) {
Map<Integer, String> commands = new HashMap<>();
try {
String mode = ((StringType) cmd).toString();
switch (RemoteSceneButtonConfig.valueOf(mode)) {
case BUTTON_4:
commands.put(0x0F, "grouped ON");
commands.put(0x09, "toggle off ON");
break;
case BUTTON_8_ALWAYS_ON:
commands.put(0x0E, "grouped OFF");
commands.put(0x09, "toggle off ON");
break;
case BUTTON_8_TOGGLE:
commands.put(0x0E, "grouped OFF");
commands.put(0x08, "toggle off OFF");
break;
}
} catch (IllegalArgumentException e) {
logger.warn("{}: got unexpected button config command: {}, ignoring request", nm(), cmd);
}
return commands;
}
}
/**
* Remote switch button config command handler
*/
public static class RemoteSwitchButtonConfigCommandHandler extends MultiOpFlagsCommandHandler {
RemoteSwitchButtonConfigCommandHandler(DeviceFeature feature) {
super(feature);
}
@Override
protected Map<Integer, String> getOpFlagCommands(Command cmd) {
Map<Integer, String> commands = new HashMap<>();
try {
String mode = ((StringType) cmd).toString();
switch (RemoteSwitchButtonConfig.valueOf(mode)) {
case BUTTON_1:
commands.put(0x0F, "grouped ON");
commands.put(0x09, "toggle off ON");
break;
case BUTTON_2_ALWAYS_ON:
commands.put(0x0E, "grouped OFF");
commands.put(0x09, "toggle off ON");
break;
case BUTTON_2_TOGGLE:
commands.put(0x0E, "grouped OFF");
commands.put(0x08, "toggle off OFF");
break;
}
} catch (IllegalArgumentException e) {
logger.warn("{}: got unexpected button config command: {}, ignoring request", nm(), cmd);
}
return commands;
}
}
/**
* Sprinkler valve on/off command handler
*/

View File

@ -16,6 +16,7 @@ import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import org.eclipse.jdt.annotation.NonNullByDefault;
@ -111,24 +112,27 @@ public class FeatureEnums {
}
}
public static enum KeypadButtonConfig {
BUTTON_6(0x07, 6),
BUTTON_8(0x06, 8);
public static enum KeypadButtonConfig implements DeviceTypeRenamer {
BUTTON_6(false, "KeypadButton6"),
BUTTON_8(true, "KeypadButton8");
private int value;
private int count;
private static final Pattern DEVICE_TYPE_NAME_PATTERN = Pattern.compile("KeypadButton[68]$");
private KeypadButtonConfig(int value, int count) {
this.value = value;
this.count = count;
private boolean setFlag;
private String replacement;
private KeypadButtonConfig(boolean setFlag, String replacement) {
this.setFlag = setFlag;
this.replacement = replacement;
}
public int getValue() {
return value;
@Override
public String getNewDeviceType(String deviceType) {
return DEVICE_TYPE_NAME_PATTERN.matcher(deviceType).replaceAll(replacement);
}
public int getCount() {
return count;
public boolean shouldSetFlag() {
return setFlag;
}
public static KeypadButtonConfig from(boolean is8Button) {
@ -194,6 +198,78 @@ public class FeatureEnums {
}
}
public static enum RemoteSceneButtonConfig implements DeviceTypeRenamer {
BUTTON_4("MiniRemoteScene4"),
BUTTON_8_ALWAYS_ON("MiniRemoteScene8"),
BUTTON_8_TOGGLE("MiniRemoteScene8");
private static final Pattern DEVICE_TYPE_NAME_PATTERN = Pattern.compile("MiniRemoteScene[48]$");
private String replacement;
private RemoteSceneButtonConfig(String replacement) {
this.replacement = replacement;
}
@Override
public String getNewDeviceType(String deviceType) {
return DEVICE_TYPE_NAME_PATTERN.matcher(deviceType).replaceAll(replacement);
}
public static RemoteSceneButtonConfig valueOf(int value) {
if (BinaryUtils.isBitSet(value, 6)) {
// return button 4, when grouped op flag (6) is on
return RemoteSceneButtonConfig.BUTTON_4;
} else if (BinaryUtils.isBitSet(value, 4)) {
// return button 8 always on, when toggle off op flag (5) is on
return RemoteSceneButtonConfig.BUTTON_8_ALWAYS_ON;
} else {
// return button 8 toggle, otherwise
return RemoteSceneButtonConfig.BUTTON_8_TOGGLE;
}
}
public static List<String> names() {
return Arrays.stream(values()).map(String::valueOf).toList();
}
}
public static enum RemoteSwitchButtonConfig implements DeviceTypeRenamer {
BUTTON_1("MiniRemoteSwitch"),
BUTTON_2_ALWAYS_ON("MiniRemoteSwitch2"),
BUTTON_2_TOGGLE("MiniRemoteSwitch2");
private static final Pattern DEVICE_TYPE_NAME_PATTERN = Pattern.compile("MiniRemoteSwitch[2]?$");
private String replacement;
private RemoteSwitchButtonConfig(String replacement) {
this.replacement = replacement;
}
@Override
public String getNewDeviceType(String deviceType) {
return DEVICE_TYPE_NAME_PATTERN.matcher(deviceType).replaceAll(replacement);
}
public static RemoteSwitchButtonConfig valueOf(int value) {
if (BinaryUtils.isBitSet(value, 6)) {
// return button 1, when grouped op flag (6) is on
return RemoteSwitchButtonConfig.BUTTON_1;
} else if (BinaryUtils.isBitSet(value, 4)) {
// return button 2 always on, when toggle off op flag (5) is on
return RemoteSwitchButtonConfig.BUTTON_2_ALWAYS_ON;
} else {
// return button 2 toggle, otherwise
return RemoteSwitchButtonConfig.BUTTON_2_TOGGLE;
}
}
public static List<String> names() {
return Arrays.stream(values()).map(String::valueOf).toList();
}
}
public static enum SirenAlertType {
CHIME(0x00),
LOUD_SIREN(0x01);
@ -401,4 +477,8 @@ public class FeatureEnums {
return format;
}
}
public interface DeviceTypeRenamer {
String getNewDeviceType(String deviceType);
}
}

View File

@ -33,8 +33,6 @@ import javax.measure.quantity.Time;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.insteon.internal.device.DeviceFeature;
import org.openhab.binding.insteon.internal.device.DeviceType;
import org.openhab.binding.insteon.internal.device.DeviceTypeRegistry;
import org.openhab.binding.insteon.internal.device.InsteonEngine;
import org.openhab.binding.insteon.internal.device.RampRate;
import org.openhab.binding.insteon.internal.device.feature.FeatureEnums.ButtonEvent;
@ -44,6 +42,8 @@ import org.openhab.binding.insteon.internal.device.feature.FeatureEnums.IOLincRe
import org.openhab.binding.insteon.internal.device.feature.FeatureEnums.KeypadButtonConfig;
import org.openhab.binding.insteon.internal.device.feature.FeatureEnums.KeypadButtonToggleMode;
import org.openhab.binding.insteon.internal.device.feature.FeatureEnums.MicroModuleOpMode;
import org.openhab.binding.insteon.internal.device.feature.FeatureEnums.RemoteSceneButtonConfig;
import org.openhab.binding.insteon.internal.device.feature.FeatureEnums.RemoteSwitchButtonConfig;
import org.openhab.binding.insteon.internal.device.feature.FeatureEnums.SirenAlertType;
import org.openhab.binding.insteon.internal.device.feature.FeatureEnums.ThermostatFanMode;
import org.openhab.binding.insteon.internal.device.feature.FeatureEnums.ThermostatSystemMode;
@ -52,7 +52,6 @@ import org.openhab.binding.insteon.internal.device.feature.FeatureEnums.Thermost
import org.openhab.binding.insteon.internal.device.feature.FeatureEnums.ThermostatTimeFormat;
import org.openhab.binding.insteon.internal.device.feature.FeatureEnums.VenstarSystemMode;
import org.openhab.binding.insteon.internal.transport.message.FieldException;
import org.openhab.binding.insteon.internal.transport.message.GroupMessageStateMachine.GroupMessageType;
import org.openhab.binding.insteon.internal.transport.message.Msg;
import org.openhab.binding.insteon.internal.utils.BinaryUtils;
import org.openhab.binding.insteon.internal.utils.HexUtils;
@ -146,29 +145,11 @@ public abstract class MessageHandler extends BaseFeatureHandler {
* Returns if an incoming message is a duplicate
*
* @param msg the received message
* @return true if the broadcast message is a duplicate
* @return true if group or broadcast message is duplicate
*/
protected boolean isDuplicate(Msg msg) {
try {
if (msg.isAllLinkBroadcastOrCleanup()) {
byte cmd1 = msg.getByte("command1");
long timestamp = msg.getTimestamp();
int group = msg.getGroup();
GroupMessageType type = msg.isAllLinkBroadcast() ? GroupMessageType.BCAST : GroupMessageType.CLEAN;
if (msg.isAllLinkSuccessReport()) {
cmd1 = msg.getInsteonAddress("toAddress").getHighByte();
type = GroupMessageType.SUCCESS;
}
return getInsteonDevice().isDuplicateGroupMsg(cmd1, timestamp, group, type);
} else if (msg.isBroadcast()) {
byte cmd1 = msg.getByte("command1");
long timestamp = msg.getTimestamp();
return getInsteonDevice().isDuplicateBroadcastMsg(cmd1, timestamp);
}
} catch (IllegalArgumentException e) {
logger.warn("cannot parse msg: {}", msg, e);
} catch (FieldException e) {
logger.warn("cannot parse msg: {}", msg, e);
if (msg.isAllLinkBroadcastOrCleanup() || msg.isBroadcast()) {
return getInsteonDevice().isDuplicateMsg(msg);
}
return false;
}
@ -236,13 +217,9 @@ public abstract class MessageHandler extends BaseFeatureHandler {
* @throws FieldException if field not there
*/
private boolean matchesParameter(Msg msg, String field, String param) throws FieldException {
int mp = getParameterAsInteger(param, -1);
int value = getParameterAsInteger(param, -1);
// parameter not filtered for, declare this a match!
if (mp == -1) {
return true;
}
byte value = msg.getByte(field);
return value == mp;
return value == -1 || msg.getInt(field) == value;
}
/**
@ -993,11 +970,16 @@ public abstract class MessageHandler extends BaseFeatureHandler {
public void handleMessage(byte cmd1, Msg msg) {
// trigger poll if is my command reply message (0x20)
if (feature.getQueryCommand() == 0x20) {
feature.triggerPoll(0L);
long delay = getPollDelay();
feature.triggerPoll(delay);
} else {
super.handleMessage(cmd1, msg);
}
}
protected long getPollDelay() {
return 0L;
}
}
/**
@ -1043,26 +1025,11 @@ public abstract class MessageHandler extends BaseFeatureHandler {
@Override
protected State getBitState(boolean is8Button) {
KeypadButtonConfig config = KeypadButtonConfig.from(is8Button);
// update device type based on button count
updateDeviceType(config.getCount());
// update device type based on button config
getInsteonDevice().updateType(config);
// return button config state
return new StringType(config.toString());
}
private void updateDeviceType(int buttonCount) {
DeviceType deviceType = getInsteonDevice().getType();
if (deviceType == null) {
logger.warn("{}: unknown device type for {}", nm(), getInsteonDevice().getAddress());
} else {
String name = deviceType.getName().replaceAll(".$", String.valueOf(buttonCount));
DeviceType newType = DeviceTypeRegistry.getInstance().getDeviceType(name);
if (newType == null) {
logger.warn("{}: unknown device type {}", nm(), name);
} else {
getInsteonDevice().updateType(newType);
}
}
}
}
/**
@ -1107,13 +1074,6 @@ public abstract class MessageHandler extends BaseFeatureHandler {
@Override
public void handleMessage(byte cmd1, Msg msg) {
super.handleMessage(cmd1, msg);
// poll battery powered sensor device while awake
if (getInsteonDevice().isBatteryPowered()) {
// no delay for all link cleanup, all link success report or replayed messages
// otherise, 1500ms for all link broadcast message allowing cleanup msg to be be processed beforehand
long delay = msg.isAllLinkCleanup() || msg.isAllLinkSuccessReport() || msg.isReplayed() ? 0L : 1500L;
getInsteonDevice().doPoll(delay);
}
// poll related devices
feature.pollRelatedDevices(0L);
}
@ -1339,19 +1299,14 @@ public abstract class MessageHandler extends BaseFeatureHandler {
/**
* I/O linc relay mode reply message handler
*/
public static class IOLincRelayModeReplyHandler extends CustomMsgHandler {
public static class IOLincRelayModeReplyHandler extends OpFlagsReplyHandler {
IOLincRelayModeReplyHandler(DeviceFeature feature) {
super(feature);
}
@Override
public void handleMessage(byte cmd1, Msg msg) {
// trigger poll if is my command reply message (0x20)
if (feature.getQueryCommand() == 0x20) {
feature.triggerPoll(5000L); // 5000ms delay to allow all op flag commands to be processed
} else {
super.handleMessage(cmd1, msg);
}
protected long getPollDelay() {
return 5000L; // delay to allow all op flag commands to be processed
}
@Override
@ -1364,19 +1319,14 @@ public abstract class MessageHandler extends BaseFeatureHandler {
/**
* Micro module operation mode reply message handler
*/
public static class MicroModuleOpModeReplyHandler extends CustomMsgHandler {
public static class MicroModuleOpModeReplyHandler extends OpFlagsReplyHandler {
MicroModuleOpModeReplyHandler(DeviceFeature feature) {
super(feature);
}
@Override
public void handleMessage(byte cmd1, Msg msg) {
// trigger poll if is my command reply message (0x20)
if (feature.getQueryCommand() == 0x20) {
feature.triggerPoll(2000L); // 2000ms delay to allow all op flag commands to be processed
} else {
super.handleMessage(cmd1, msg);
}
protected long getPollDelay() {
return 2000L; // delay to allow all op flag commands to be processed
}
@Override
@ -1445,6 +1395,52 @@ public abstract class MessageHandler extends BaseFeatureHandler {
}
}
/**
* Remote scene button config reply message handler
*/
public static class RemoteSceneButtonConfigReplyHandler extends OpFlagsReplyHandler {
RemoteSceneButtonConfigReplyHandler(DeviceFeature feature) {
super(feature);
}
@Override
protected long getPollDelay() {
return 2000L; // delay to allow all op flag commands to be processed
}
@Override
protected @Nullable State getState(byte cmd1, double value) {
RemoteSceneButtonConfig config = RemoteSceneButtonConfig.valueOf((int) value);
// update device type based on button config
getInsteonDevice().updateType(config);
// return button config state
return new StringType(config.toString());
}
}
/**
* Remote switch button config reply message handler
*/
public static class RemoteSwitchButtonConfigReplyHandler extends OpFlagsReplyHandler {
RemoteSwitchButtonConfigReplyHandler(DeviceFeature feature) {
super(feature);
}
@Override
protected long getPollDelay() {
return 2000L; // delay to allow all op flag commands to be processed
}
@Override
protected @Nullable State getState(byte cmd1, double value) {
RemoteSwitchButtonConfig config = RemoteSwitchButtonConfig.valueOf((int) value);
// update device type based on button config
getInsteonDevice().updateType(config);
// return button config state
return new StringType(config.toString());
}
}
/**
* Siren request reply message handler
*/

View File

@ -19,7 +19,6 @@ import java.util.Map;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
@ -28,7 +27,7 @@ import org.openhab.binding.insteon.internal.config.InsteonBridgeConfiguration;
import org.openhab.binding.insteon.internal.config.InsteonDeviceConfiguration;
import org.openhab.binding.insteon.internal.device.Device;
import org.openhab.binding.insteon.internal.device.DeviceCache;
import org.openhab.binding.insteon.internal.device.DeviceType;
import org.openhab.binding.insteon.internal.device.DeviceFeature;
import org.openhab.binding.insteon.internal.device.InsteonAddress;
import org.openhab.binding.insteon.internal.device.InsteonDevice;
import org.openhab.binding.insteon.internal.device.InsteonEngine;
@ -142,30 +141,30 @@ public class InsteonDeviceHandler extends InsteonBaseThingHandler {
@Override
protected void initializeChannels(Device device) {
DeviceType deviceType = device.getType();
if (deviceType == null) {
return;
}
super.initializeChannels(device);
getThing().getChannels().forEach(channel -> setChannelCustomSettings(channel, deviceType.getName()));
getThing().getChannels().forEach(channel -> setChannelCustomSettings(channel, device));
}
private void setChannelCustomSettings(Channel channel, String deviceTypeName) {
private void setChannelCustomSettings(Channel channel, Device device) {
ChannelUID channelUID = channel.getUID();
ChannelTypeUID channelTypeUID = channel.getChannelTypeUID();
if (channelTypeUID == null) {
return;
}
String key = deviceTypeName + ":" + channelIdToFeatureName(channelTypeUID.getId());
String[] stateDescriptionOptions = CUSTOM_STATE_DESCRIPTION_OPTIONS.get(key);
String featureName = channelIdToFeatureName(channelTypeUID.getId());
DeviceFeature feature = device.getFeature(featureName);
if (feature == null) {
return;
}
List<String> stateDescriptionOptions = CUSTOM_STATE_DESCRIPTION_OPTIONS.get(feature.getType());
if (stateDescriptionOptions == null) {
return;
}
List<StateOption> options = Stream.of(stateDescriptionOptions).map(value -> new StateOption(value,
List<StateOption> options = stateDescriptionOptions.stream().map(value -> new StateOption(value,
StringUtils.capitalizeByWhitespace(value.replace("_", " ").toLowerCase()))).toList();
logger.trace("setting state options for {} to {}", channelUID, options);

View File

@ -13,9 +13,6 @@
package org.openhab.binding.insteon.internal.transport.message;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.insteon.internal.device.InsteonAddress;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Ideally, Insteon ALL LINK messages are received in this order, and
@ -87,7 +84,7 @@ public class GroupMessageStateMachine {
* IN:Cmd:0x50|fromAddress:20.AC.99|toAddress:13.03.01|messageFlags:0xCB=ALL_LINK_BROADCAST:3:2|command1:0x06|
* command2:0x00|
*/
public static enum GroupMessageType {
private enum GroupMessageType {
BCAST,
CLEAN,
SUCCESS
@ -97,97 +94,89 @@ public class GroupMessageStateMachine {
* The state of the machine (i.e. what message we are expecting next).
* The usual state should be EXPECT_BCAST
*/
private static enum State {
private enum State {
EXPECT_BCAST,
EXPECT_CLEAN,
EXPECT_SUCCESS
}
private final Logger logger = LoggerFactory.getLogger(GroupMessageStateMachine.class);
private State state = State.EXPECT_BCAST;
private boolean duplicate = false;
private byte lastCmd1 = 0;
private long lastTimestamp = 0;
public boolean isDuplicate() {
/**
* Returns if group message is duplicate
*
* @param msg the group message
* @return true if the group message is duplicate
* @throws FieldException
*/
public boolean isDuplicate(Msg msg) throws FieldException {
byte cmd1 = msg.isAllLinkSuccessReport() ? msg.getInsteonAddress("toAddress").getHighByte()
: msg.getByte("command1");
long timestamp = msg.getTimestamp();
if (cmd1 != lastCmd1 || timestamp != lastTimestamp) {
GroupMessageType type = msg.isAllLinkSuccessReport() ? GroupMessageType.SUCCESS
: msg.isAllLinkCleanup() ? GroupMessageType.CLEAN : GroupMessageType.BCAST;
update(cmd1, timestamp, type);
}
return duplicate;
}
public byte getLastCommand() {
return lastCmd1;
}
public long getLastTimestamp() {
return lastTimestamp;
}
/**
* Updates the state machine and determine if not duplicate
* Updates the state machine
*
* @param address the address of the device that this state machine belongs to
* @param group the group that this state machine belongs to
* @param cmd1 cmd1 from the message received
* @param timestamp timestamp from the message received
* @param type the group message type that was received
* @return true if the group message is duplicate
*/
public boolean update(InsteonAddress address, int group, byte cmd1, long timestamp, GroupMessageType type) {
boolean isNewGroupMsg = cmd1 != lastCmd1 || timestamp > lastTimestamp + GROUP_STATE_TIMEOUT;
switch (state) {
case EXPECT_BCAST:
switch (type) {
case BCAST:
duplicate = false;
break;
case CLEAN:
case SUCCESS:
duplicate = !isNewGroupMsg;
break;
}
break;
case EXPECT_CLEAN:
switch (type) {
case BCAST:
duplicate = !isNewGroupMsg;
break;
case CLEAN:
case SUCCESS:
duplicate = true;
break;
}
break;
case EXPECT_SUCCESS:
switch (type) {
case BCAST:
duplicate = false;
break;
case CLEAN:
case SUCCESS:
duplicate = true;
break;
}
break;
}
private void update(byte cmd1, long timestamp, GroupMessageType type) {
boolean isNewGroupMsg = cmd1 != lastCmd1 || Math.abs(timestamp - lastTimestamp) > GROUP_STATE_TIMEOUT;
switch (type) {
case BCAST:
switch (state) {
case EXPECT_BCAST:
case EXPECT_SUCCESS:
duplicate = false;
break;
case EXPECT_CLEAN:
duplicate = !isNewGroupMsg;
break;
}
state = State.EXPECT_CLEAN;
break;
case CLEAN:
switch (state) {
case EXPECT_BCAST:
duplicate = !isNewGroupMsg;
break;
case EXPECT_CLEAN:
case EXPECT_SUCCESS:
duplicate = true;
break;
}
state = State.EXPECT_SUCCESS;
break;
case SUCCESS:
switch (state) {
case EXPECT_BCAST:
duplicate = !isNewGroupMsg;
break;
case EXPECT_CLEAN:
case EXPECT_SUCCESS:
duplicate = true;
break;
}
state = State.EXPECT_BCAST;
break;
}
lastCmd1 = cmd1;
lastTimestamp = timestamp;
logger.debug("{} group:{} type:{} state:{} duplicate:{}", address, group, type, state, duplicate);
return duplicate;
}
}

View File

@ -190,8 +190,14 @@ channel-type.insteon.button-beep.label = Button Beep
channel-type.insteon.button-beep.description = Enable beep on button press.
channel-type.insteon.button-config.label = Button Config
channel-type.insteon.button-config.description = Configure the button/scene mode.
channel-type.insteon.button-config.state.option.BUTTON_1 = 1-Button
channel-type.insteon.button-config.state.option.BUTTON_2_ALWAYS_ON = 2-Button Always On
channel-type.insteon.button-config.state.option.BUTTON_2_TOGGLE = 2-Button Toggle
channel-type.insteon.button-config.state.option.BUTTON_4 = 4-Button
channel-type.insteon.button-config.state.option.BUTTON_6 = 6-Button
channel-type.insteon.button-config.state.option.BUTTON_8 = 8-Button
channel-type.insteon.button-config.state.option.BUTTON_8_ALWAYS_ON = 8-Button Always On
channel-type.insteon.button-config.state.option.BUTTON_8_TOGGLE = 8-Button Toggle
channel-type.insteon.button-lock.label = Button Lock
channel-type.insteon.button-lock.description = Disable the front button press.
channel-type.insteon.carbon-monoxide-alarm.label = Carbon Monoxide Alarm
@ -308,6 +314,9 @@ channel-type.insteon.system-mode.state.option.HEAT = Heat
channel-type.insteon.system-mode.state.option.COOL = Cool
channel-type.insteon.system-mode.state.option.AUTO = Auto
channel-type.insteon.system-mode.state.option.PROGRAM = Program
channel-type.insteon.system-mode.state.option.PROGRAM_HEAT = Program Heat
channel-type.insteon.system-mode.state.option.PROGRAM_COOL = Program Cool
channel-type.insteon.system-mode.state.option.PROGRAM_AUTO = Program Heat
channel-type.insteon.system-state.label = System State
channel-type.insteon.system-state.state.option.OFF = Off
channel-type.insteon.system-state.state.option.COOLING = Cooling

View File

@ -235,7 +235,7 @@
<message-dispatcher>DefaultDispatcher</message-dispatcher>
<message-handler command="0x19">IOLincRelayModeReplyHandler</message-handler>
<message-handler default="true">NoOpMsgHandler</message-handler>
<command-handler command="OnOffType">IOLincRelayModeCommandHandler</command-handler>
<command-handler command="StringType">IOLincRelayModeCommandHandler</command-handler>
<command-handler command="RefreshType">RefreshCommandHandler</command-handler>
<poll-handler>NoPollHandler</poll-handler> <!-- polled by OpFlagsGroup -->
</feature-type>
@ -525,6 +525,34 @@
<poll-handler>NoPollHandler</poll-handler> <!-- polled by OutletStatusGroup -->
</feature-type>
<feature-type name="RemoteBatteryLevel">
<message-dispatcher>DefaultDispatcher</message-dispatcher>
<!-- battery level range 0xA0 => 0xB4 (undocumented) -->
<!-- message field data1 0x01 (documented); 0x00 (observed) -->
<message-handler command="0x2E" ext="1" cmd1="0x2E" cmd2="0x00" d1="0x00" d2="0x01" field="userData10"
min="0xA0" max="0xB4">CustomDimensionlessMsgHandler</message-handler>
<message-handler default="true">NoOpMsgHandler</message-handler>
<command-handler command="RefreshType">RefreshCommandHandler</command-handler>
<command-handler default="true">NoOpCommandHandler</command-handler>
<poll-handler>NoPollHandler</poll-handler> <!-- polled by ExtDataGroup -->
</feature-type>
<feature-type name="RemoteSceneButtonConfig">
<message-dispatcher>DefaultDispatcher</message-dispatcher>
<message-handler command="0x19">RemoteSceneButtonConfigReplyHandler</message-handler>
<message-handler default="true">NoOpMsgHandler</message-handler>
<command-handler command="StringType">RemoteSceneButtonConfigCommandHandler</command-handler>
<command-handler command="RefreshType">RefreshCommandHandler</command-handler>
<poll-handler>NoPollHandler</poll-handler> <!-- polled by OpFlagsGroup -->
</feature-type>
<feature-type name="RemoteSwitchButtonConfig">
<message-dispatcher>DefaultDispatcher</message-dispatcher>
<message-handler command="0x19">RemoteSwitchButtonConfigReplyHandler</message-handler>
<message-handler default="true">NoOpMsgHandler</message-handler>
<command-handler command="StringType">RemoteSwitchButtonConfigCommandHandler</command-handler>
<command-handler command="RefreshType">RefreshCommandHandler</command-handler>
<poll-handler>NoPollHandler</poll-handler> <!-- polled by OpFlagsGroup -->
</feature-type>
<feature-type name="PowerMeterDataGroup">
<message-dispatcher>PollGroupDispatcher</message-dispatcher>
<poll-handler ext="0" cmd1="0x82" cmd2="0x00">FlexPollHandler</poll-handler>

View File

@ -49,11 +49,15 @@
<feature name="eventButtonB" group="2">GenericButtonEvent</feature>
<feature name="eventButtonC" group="3">GenericButtonEvent</feature>
<feature name="eventButtonD" group="4">GenericButtonEvent</feature>
<feature-group name="extDataGroup" type="ExtDataGroup">
<feature name="batteryLevel">RemoteBatteryLevel</feature>
</feature-group>
<feature-group name="opFlagsGroup" type="OpFlagsGroup">
<feature name="programLock" bit="0" on="0x00" off="0x01">OpFlags</feature>
<feature name="ledOnOff" bit="1" on="0x02" off="0x03" inverted="true">OpFlags</feature>
<feature name="buttonBeep" bit="2" on="0x04" off="0x05">OpFlags</feature>
<feature name="stayAwake" bit="3" on="0x06" off="0x07">OpFlags</feature>
<feature name="buttonConfig">RemoteSceneButtonConfig</feature>
</feature-group>
<default-link name="buttonA" type="controller" group="1" data1="0x03" data2="0x00" data3="0x00"/>
<default-link name="buttonB" type="controller" group="2" data1="0x03" data2="0x00" data3="0x00"/>
@ -62,41 +66,66 @@
</device-type>
<device-type name="GeneralizedController_MiniRemoteScene8" batteryPowered="true">
<feature name="eventButtonA" group="1">GenericButtonEvent</feature>
<feature name="eventButtonB" group="2">GenericButtonEvent</feature>
<feature name="eventButtonC" group="3">GenericButtonEvent</feature>
<feature name="eventButtonD" group="4">GenericButtonEvent</feature>
<feature name="eventButtonE" group="5">GenericButtonEvent</feature>
<feature name="eventButtonF" group="6">GenericButtonEvent</feature>
<feature name="eventButtonG" group="7">GenericButtonEvent</feature>
<feature name="eventButtonH" group="8">GenericButtonEvent</feature>
<feature name="eventButtonA" group="2">GenericButtonEvent</feature>
<feature name="eventButtonB" group="1">GenericButtonEvent</feature>
<feature name="eventButtonC" group="4">GenericButtonEvent</feature>
<feature name="eventButtonD" group="3">GenericButtonEvent</feature>
<feature name="eventButtonE" group="6">GenericButtonEvent</feature>
<feature name="eventButtonF" group="5">GenericButtonEvent</feature>
<feature name="eventButtonG" group="8">GenericButtonEvent</feature>
<feature name="eventButtonH" group="7">GenericButtonEvent</feature>
<feature-group name="extDataGroup" type="ExtDataGroup">
<feature name="batteryLevel">RemoteBatteryLevel</feature>
</feature-group>
<feature-group name="opFlagsGroup" type="OpFlagsGroup">
<feature name="programLock" bit="0" on="0x00" off="0x01">OpFlags</feature>
<feature name="ledOnOff" bit="1" on="0x02" off="0x03" inverted="true">OpFlags</feature>
<feature name="buttonBeep" bit="2" on="0x04" off="0x05">OpFlags</feature>
<feature name="stayAwake" bit="3" on="0x06" off="0x07">OpFlags</feature>
<feature name="buttonConfig">RemoteSceneButtonConfig</feature>
</feature-group>
<default-link name="buttonA" type="controller" group="1" data1="0x03" data2="0x00" data3="0x00"/>
<default-link name="buttonB" type="controller" group="2" data1="0x03" data2="0x00" data3="0x00"/>
<default-link name="buttonC" type="controller" group="3" data1="0x03" data2="0x00" data3="0x00"/>
<default-link name="buttonD" type="controller" group="4" data1="0x03" data2="0x00" data3="0x00"/>
<default-link name="buttonE" type="controller" group="5" data1="0x03" data2="0x00" data3="0x00"/>
<default-link name="buttonF" type="controller" group="6" data1="0x03" data2="0x00" data3="0x00"/>
<default-link name="buttonG" type="controller" group="7" data1="0x03" data2="0x00" data3="0x00"/>
<default-link name="buttonH" type="controller" group="8" data1="0x03" data2="0x00" data3="0x00"/>
<default-link name="buttonA" type="controller" group="2" data1="0x03" data2="0x00" data3="0x00"/>
<default-link name="buttonB" type="controller" group="1" data1="0x03" data2="0x00" data3="0x00"/>
<default-link name="buttonC" type="controller" group="4" data1="0x03" data2="0x00" data3="0x00"/>
<default-link name="buttonD" type="controller" group="3" data1="0x03" data2="0x00" data3="0x00"/>
<default-link name="buttonE" type="controller" group="6" data1="0x03" data2="0x00" data3="0x00"/>
<default-link name="buttonF" type="controller" group="5" data1="0x03" data2="0x00" data3="0x00"/>
<default-link name="buttonG" type="controller" group="8" data1="0x03" data2="0x00" data3="0x00"/>
<default-link name="buttonH" type="controller" group="7" data1="0x03" data2="0x00" data3="0x00"/>
</device-type>
<device-type name="GeneralizedController_MiniRemoteSwitch" batteryPowered="true">
<feature name="eventButton">GenericButtonEvent</feature>
<feature-group name="extDataGroup" type="ExtDataGroup">
<feature name="batteryLevel">RemoteBatteryLevel</feature>
</feature-group>
<feature-group name="opFlagsGroup" type="OpFlagsGroup">
<feature name="programLock" bit="0" on="0x00" off="0x01">OpFlags</feature>
<feature name="ledOnOff" bit="1" on="0x02" off="0x03" inverted="true">OpFlags</feature>
<feature name="buttonBeep" bit="2" on="0x04" off="0x05">OpFlags</feature>
<feature name="stayAwake" bit="3" on="0x06" off="0x07">OpFlags</feature>
<feature name="buttonConfig">RemoteSwitchButtonConfig</feature>
</feature-group>
<default-link name="button" type="controller" group="1" data1="0x03" data2="0x00" data3="0x00"/>
</device-type>
<device-type name="GeneralizedController_MiniRemoteSwitch2" batteryPowered="true">
<feature name="eventButtonA" group="1">GenericButtonEvent</feature>
<feature name="eventButtonB" group="2">GenericButtonEvent</feature>
<feature-group name="extDataGroup" type="ExtDataGroup">
<feature name="batteryLevel">RemoteBatteryLevel</feature>
</feature-group>
<feature-group name="opFlagsGroup" type="OpFlagsGroup">
<feature name="programLock" bit="0" on="0x00" off="0x01">OpFlags</feature>
<feature name="ledOnOff" bit="1" on="0x02" off="0x03" inverted="true">OpFlags</feature>
<feature name="buttonBeep" bit="2" on="0x04" off="0x05">OpFlags</feature>
<feature name="stayAwake" bit="3" on="0x06" off="0x07">OpFlags</feature>
<feature name="buttonConfig">RemoteSwitchButtonConfig</feature>
</feature-group>
<default-link name="buttonA" type="controller" group="1" data1="0x03" data2="0x00" data3="0x00"/>
<default-link name="buttonB" type="controller" group="2" data1="0x03" data2="0x00" data3="0x00"/>
</device-type>
<!-- Dimmable Lighting Control -->
<device-type name="DimmableLightingControl">