[insteon] Update remote device support (#17540)

* [insteon] Fix remote device not polled when awake

Signed-off-by: jsetton <jeremy.setton@gmail.com>
pull/17456/head
Jeremy 2024-10-10 15:02:52 -04:00 committed by GitHub
parent d923eb97ce
commit fbf61e636c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 426 additions and 224 deletions

View File

@ -13,10 +13,13 @@
package org.openhab.binding.insteon.internal; package org.openhab.binding.insteon.internal;
import java.io.File; import java.io.File;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault; 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.binding.insteon.internal.device.feature.FeatureEnums.VenstarSystemMode;
import org.openhab.core.OpenHAB; import org.openhab.core.OpenHAB;
import org.openhab.core.thing.ThingTypeUID; 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_RAMP_RATE = "rampRate";
public static final String FEATURE_SCENE_ON_OFF = "sceneOnOff"; public static final String FEATURE_SCENE_ON_OFF = "sceneOnOff";
public static final String FEATURE_STAY_AWAKE = "stayAwake"; 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_TEMPERATURE_SCALE = "temperatureScale";
public static final String FEATURE_TWO_GROUPS = "2Groups"; 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_ON_MASK = "KeypadButtonOnMask";
public static final String FEATURE_TYPE_KEYPAD_BUTTON_TOGGLE_MODE = "KeypadButtonToggleMode"; 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_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_FAN_MODE = "ThermostatFanMode";
public static final String FEATURE_TYPE_THERMOSTAT_SYSTEM_MODE = "ThermostatSystemMode"; public static final String FEATURE_TYPE_THERMOSTAT_SYSTEM_MODE = "ThermostatSystemMode";
public static final String FEATURE_TYPE_THERMOSTAT_COOL_SETPOINT = "ThermostatCoolSetpoint"; 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_COOL_SETPOINT = "VenstarCoolSetpoint";
public static final String FEATURE_TYPE_VENSTAR_HEAT_SETPOINT = "VenstarHeatSetpoint"; 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 // Map of custom state description options
public static final Map<String, String[]> CUSTOM_STATE_DESCRIPTION_OPTIONS = Map.ofEntries( public static final Map<String, List<String>> CUSTOM_STATE_DESCRIPTION_OPTIONS = Map.ofEntries(
// Venstar Thermostat System Mode Map.entry(FEATURE_TYPE_REMOTE_SCENE_BUTTON_CONFIG, RemoteSceneButtonConfig.names()),
Map.entry(DEVICE_TYPE_CLIMATE_CONTROL_VENSTAR_THERMOSTAT + ":" + FEATURE_SYSTEM_MODE, Map.entry(FEATURE_TYPE_REMOTE_SWITCH_BUTTON_CONFIG, RemoteSwitchButtonConfig.names()),
VenstarSystemMode.names().toArray(String[]::new))); 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.ModemDBChange;
import org.openhab.binding.insteon.internal.device.database.ModemDBEntry; 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.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.device.feature.FeatureEnums.KeypadButtonToggleMode;
import org.openhab.binding.insteon.internal.handler.InsteonDeviceHandler; 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;
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.transport.message.Msg;
import org.openhab.binding.insteon.internal.utils.BinaryUtils; import org.openhab.binding.insteon.internal.utils.BinaryUtils;
import org.openhab.core.library.types.DecimalType; 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 msg the message received
* @param timestamp the timestamp from the broadcast message received * @return true if group or broadcast message is duplicate
* @return true if the broadcast message is duplicate
*/ */
public boolean isDuplicateBroadcastMsg(byte cmd1, long timestamp) { public boolean isDuplicateMsg(Msg msg) {
synchronized (lastBroadcastReceived) { try {
long timelapse = timestamp - lastBroadcastReceived.getOrDefault(cmd1, timestamp); if (msg.isAllLinkBroadcastOrCleanup()) {
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) { synchronized (groupState) {
GroupMessageStateMachine stateMachine = groupState.get(group); int group = msg.getGroup();
if (stateMachine == null) { GroupMessageStateMachine stateMachine = groupState.computeIfAbsent(group,
stateMachine = new GroupMessageStateMachine(); k -> new GroupMessageStateMachine());
groupState.put(group, stateMachine); return stateMachine != null && stateMachine.isDuplicate(msg);
logger.trace("{} created group {} state", address, group);
} }
if (stateMachine.getLastCommand() == cmd1 && stateMachine.getLastTimestamp() == timestamp) { } else if (msg.isBroadcast()) {
logger.trace("{} using previous group {} state for {}", address, group, type); synchronized (lastBroadcastReceived) {
return stateMachine.isDuplicate(); byte cmd1 = msg.getByte("command1");
} else { long timestamp = msg.getTimestamp();
logger.trace("{} updating group {} state to {}", address, group, type); Long lastTimestamp = lastBroadcastReceived.put(cmd1, timestamp);
return stateMachine.update(address, group, cmd1, timestamp, type); 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) getFeatures().stream().filter(DeviceFeature::isStatusFeature)
.forEach(feature -> feature.handleMessage(msg)); .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 // notify if responding state changed
if (isPrevResponding != isResponding()) { if (isPrevResponding != isResponding()) {
statusChanged(); 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 * Updates this device type
* *
* @param newType the new device type to use * @param newType the new device type to use
*/ */
public void updateType(DeviceType newType) { public void updateType(DeviceType newType) {
ProductData productData = getProductData(); ProductData productData = getProductData();
DeviceType currentType = getType(); 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.KeypadButtonConfig;
import org.openhab.binding.insteon.internal.device.feature.FeatureEnums.KeypadButtonToggleMode; 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.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.SirenAlertType;
import org.openhab.binding.insteon.internal.device.feature.FeatureEnums.ThermostatFanMode; import org.openhab.binding.insteon.internal.device.feature.FeatureEnums.ThermostatFanMode;
import org.openhab.binding.insteon.internal.device.feature.FeatureEnums.ThermostatSystemMode; 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) { protected int getOpFlagCommand(Command cmd) {
try { try {
String config = ((StringType) cmd).toString(); String config = ((StringType) cmd).toString();
return KeypadButtonConfig.valueOf(config).getValue(); return KeypadButtonConfig.valueOf(config).shouldSetFlag() ? getParameterAsInteger("on", -1)
: getParameterAsInteger("off", -1);
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
logger.warn("{}: got unexpected button config command: {}, ignoring request", nm(), cmd); logger.warn("{}: got unexpected button config command: {}, ignoring request", nm(), cmd);
return -1; 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 * Sprinkler valve on/off command handler
*/ */

View File

@ -16,6 +16,7 @@ import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.function.Function; import java.util.function.Function;
import java.util.regex.Pattern;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
@ -111,24 +112,27 @@ public class FeatureEnums {
} }
} }
public static enum KeypadButtonConfig { public static enum KeypadButtonConfig implements DeviceTypeRenamer {
BUTTON_6(0x07, 6), BUTTON_6(false, "KeypadButton6"),
BUTTON_8(0x06, 8); BUTTON_8(true, "KeypadButton8");
private int value; private static final Pattern DEVICE_TYPE_NAME_PATTERN = Pattern.compile("KeypadButton[68]$");
private int count;
private KeypadButtonConfig(int value, int count) { private boolean setFlag;
this.value = value; private String replacement;
this.count = count;
private KeypadButtonConfig(boolean setFlag, String replacement) {
this.setFlag = setFlag;
this.replacement = replacement;
} }
public int getValue() { @Override
return value; public String getNewDeviceType(String deviceType) {
return DEVICE_TYPE_NAME_PATTERN.matcher(deviceType).replaceAll(replacement);
} }
public int getCount() { public boolean shouldSetFlag() {
return count; return setFlag;
} }
public static KeypadButtonConfig from(boolean is8Button) { 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 { public static enum SirenAlertType {
CHIME(0x00), CHIME(0x00),
LOUD_SIREN(0x01); LOUD_SIREN(0x01);
@ -401,4 +477,8 @@ public class FeatureEnums {
return format; 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.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.insteon.internal.device.DeviceFeature; 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.InsteonEngine;
import org.openhab.binding.insteon.internal.device.RampRate; import org.openhab.binding.insteon.internal.device.RampRate;
import org.openhab.binding.insteon.internal.device.feature.FeatureEnums.ButtonEvent; 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.KeypadButtonConfig;
import org.openhab.binding.insteon.internal.device.feature.FeatureEnums.KeypadButtonToggleMode; 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.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.SirenAlertType;
import org.openhab.binding.insteon.internal.device.feature.FeatureEnums.ThermostatFanMode; import org.openhab.binding.insteon.internal.device.feature.FeatureEnums.ThermostatFanMode;
import org.openhab.binding.insteon.internal.device.feature.FeatureEnums.ThermostatSystemMode; 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.ThermostatTimeFormat;
import org.openhab.binding.insteon.internal.device.feature.FeatureEnums.VenstarSystemMode; 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.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.transport.message.Msg;
import org.openhab.binding.insteon.internal.utils.BinaryUtils; import org.openhab.binding.insteon.internal.utils.BinaryUtils;
import org.openhab.binding.insteon.internal.utils.HexUtils; 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 * Returns if an incoming message is a duplicate
* *
* @param msg the received message * @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) { protected boolean isDuplicate(Msg msg) {
try { if (msg.isAllLinkBroadcastOrCleanup() || msg.isBroadcast()) {
if (msg.isAllLinkBroadcastOrCleanup()) { return getInsteonDevice().isDuplicateMsg(msg);
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);
} }
return false; return false;
} }
@ -236,13 +217,9 @@ public abstract class MessageHandler extends BaseFeatureHandler {
* @throws FieldException if field not there * @throws FieldException if field not there
*/ */
private boolean matchesParameter(Msg msg, String field, String param) throws FieldException { 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! // parameter not filtered for, declare this a match!
if (mp == -1) { return value == -1 || msg.getInt(field) == value;
return true;
}
byte value = msg.getByte(field);
return value == mp;
} }
/** /**
@ -993,11 +970,16 @@ public abstract class MessageHandler extends BaseFeatureHandler {
public void handleMessage(byte cmd1, Msg msg) { public void handleMessage(byte cmd1, Msg msg) {
// trigger poll if is my command reply message (0x20) // trigger poll if is my command reply message (0x20)
if (feature.getQueryCommand() == 0x20) { if (feature.getQueryCommand() == 0x20) {
feature.triggerPoll(0L); long delay = getPollDelay();
feature.triggerPoll(delay);
} else { } else {
super.handleMessage(cmd1, msg); super.handleMessage(cmd1, msg);
} }
} }
protected long getPollDelay() {
return 0L;
}
} }
/** /**
@ -1043,26 +1025,11 @@ public abstract class MessageHandler extends BaseFeatureHandler {
@Override @Override
protected State getBitState(boolean is8Button) { protected State getBitState(boolean is8Button) {
KeypadButtonConfig config = KeypadButtonConfig.from(is8Button); KeypadButtonConfig config = KeypadButtonConfig.from(is8Button);
// update device type based on button count // update device type based on button config
updateDeviceType(config.getCount()); getInsteonDevice().updateType(config);
// return button config state // return button config state
return new StringType(config.toString()); 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 @Override
public void handleMessage(byte cmd1, Msg msg) { public void handleMessage(byte cmd1, Msg msg) {
super.handleMessage(cmd1, 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 // poll related devices
feature.pollRelatedDevices(0L); feature.pollRelatedDevices(0L);
} }
@ -1339,19 +1299,14 @@ public abstract class MessageHandler extends BaseFeatureHandler {
/** /**
* I/O linc relay mode reply message handler * I/O linc relay mode reply message handler
*/ */
public static class IOLincRelayModeReplyHandler extends CustomMsgHandler { public static class IOLincRelayModeReplyHandler extends OpFlagsReplyHandler {
IOLincRelayModeReplyHandler(DeviceFeature feature) { IOLincRelayModeReplyHandler(DeviceFeature feature) {
super(feature); super(feature);
} }
@Override @Override
public void handleMessage(byte cmd1, Msg msg) { protected long getPollDelay() {
// trigger poll if is my command reply message (0x20) return 5000L; // delay to allow all op flag commands to be processed
if (feature.getQueryCommand() == 0x20) {
feature.triggerPoll(5000L); // 5000ms delay to allow all op flag commands to be processed
} else {
super.handleMessage(cmd1, msg);
}
} }
@Override @Override
@ -1364,19 +1319,14 @@ public abstract class MessageHandler extends BaseFeatureHandler {
/** /**
* Micro module operation mode reply message handler * Micro module operation mode reply message handler
*/ */
public static class MicroModuleOpModeReplyHandler extends CustomMsgHandler { public static class MicroModuleOpModeReplyHandler extends OpFlagsReplyHandler {
MicroModuleOpModeReplyHandler(DeviceFeature feature) { MicroModuleOpModeReplyHandler(DeviceFeature feature) {
super(feature); super(feature);
} }
@Override @Override
public void handleMessage(byte cmd1, Msg msg) { protected long getPollDelay() {
// trigger poll if is my command reply message (0x20) return 2000L; // delay to allow all op flag commands to be processed
if (feature.getQueryCommand() == 0x20) {
feature.triggerPoll(2000L); // 2000ms delay to allow all op flag commands to be processed
} else {
super.handleMessage(cmd1, msg);
}
} }
@Override @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 * Siren request reply message handler
*/ */

View File

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

View File

@ -13,9 +13,6 @@
package org.openhab.binding.insteon.internal.transport.message; package org.openhab.binding.insteon.internal.transport.message;
import org.eclipse.jdt.annotation.NonNullByDefault; 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 * 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| * IN:Cmd:0x50|fromAddress:20.AC.99|toAddress:13.03.01|messageFlags:0xCB=ALL_LINK_BROADCAST:3:2|command1:0x06|
* command2:0x00| * command2:0x00|
*/ */
public static enum GroupMessageType { private enum GroupMessageType {
BCAST, BCAST,
CLEAN, CLEAN,
SUCCESS SUCCESS
@ -97,97 +94,89 @@ public class GroupMessageStateMachine {
* The state of the machine (i.e. what message we are expecting next). * The state of the machine (i.e. what message we are expecting next).
* The usual state should be EXPECT_BCAST * The usual state should be EXPECT_BCAST
*/ */
private static enum State { private enum State {
EXPECT_BCAST, EXPECT_BCAST,
EXPECT_CLEAN, EXPECT_CLEAN,
EXPECT_SUCCESS EXPECT_SUCCESS
} }
private final Logger logger = LoggerFactory.getLogger(GroupMessageStateMachine.class);
private State state = State.EXPECT_BCAST; private State state = State.EXPECT_BCAST;
private boolean duplicate = false; private boolean duplicate = false;
private byte lastCmd1 = 0; private byte lastCmd1 = 0;
private long lastTimestamp = 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; 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 cmd1 cmd1 from the message received
* @param timestamp timestamp from the message received * @param timestamp timestamp from the message received
* @param type the group message type that was 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) { private void update(byte cmd1, long timestamp, GroupMessageType type) {
boolean isNewGroupMsg = cmd1 != lastCmd1 || timestamp > lastTimestamp + GROUP_STATE_TIMEOUT; boolean isNewGroupMsg = cmd1 != lastCmd1 || Math.abs(timestamp - lastTimestamp) > GROUP_STATE_TIMEOUT;
switch (type) {
case BCAST:
switch (state) { switch (state) {
case EXPECT_BCAST: case EXPECT_BCAST:
switch (type) { case EXPECT_SUCCESS:
case BCAST:
duplicate = false; duplicate = false;
break; break;
case CLEAN:
case SUCCESS:
duplicate = !isNewGroupMsg;
break;
}
break;
case EXPECT_CLEAN: case EXPECT_CLEAN:
switch (type) {
case BCAST:
duplicate = !isNewGroupMsg; duplicate = !isNewGroupMsg;
break; 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;
}
switch (type) {
case BCAST:
state = State.EXPECT_CLEAN; state = State.EXPECT_CLEAN;
break; break;
case CLEAN: case CLEAN:
switch (state) {
case EXPECT_BCAST:
duplicate = !isNewGroupMsg;
break;
case EXPECT_CLEAN:
case EXPECT_SUCCESS:
duplicate = true;
break;
}
state = State.EXPECT_SUCCESS; state = State.EXPECT_SUCCESS;
break; break;
case SUCCESS: case SUCCESS:
switch (state) {
case EXPECT_BCAST:
duplicate = !isNewGroupMsg;
break;
case EXPECT_CLEAN:
case EXPECT_SUCCESS:
duplicate = true;
break;
}
state = State.EXPECT_BCAST; state = State.EXPECT_BCAST;
break; break;
} }
lastCmd1 = cmd1; lastCmd1 = cmd1;
lastTimestamp = timestamp; 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-beep.description = Enable beep on button press.
channel-type.insteon.button-config.label = Button Config channel-type.insteon.button-config.label = Button Config
channel-type.insteon.button-config.description = Configure the button/scene mode. 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_6 = 6-Button
channel-type.insteon.button-config.state.option.BUTTON_8 = 8-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.label = Button Lock
channel-type.insteon.button-lock.description = Disable the front button press. channel-type.insteon.button-lock.description = Disable the front button press.
channel-type.insteon.carbon-monoxide-alarm.label = Carbon Monoxide Alarm 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.COOL = Cool
channel-type.insteon.system-mode.state.option.AUTO = Auto 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 = 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.label = System State
channel-type.insteon.system-state.state.option.OFF = Off channel-type.insteon.system-state.state.option.OFF = Off
channel-type.insteon.system-state.state.option.COOLING = Cooling channel-type.insteon.system-state.state.option.COOLING = Cooling

View File

@ -235,7 +235,7 @@
<message-dispatcher>DefaultDispatcher</message-dispatcher> <message-dispatcher>DefaultDispatcher</message-dispatcher>
<message-handler command="0x19">IOLincRelayModeReplyHandler</message-handler> <message-handler command="0x19">IOLincRelayModeReplyHandler</message-handler>
<message-handler default="true">NoOpMsgHandler</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> <command-handler command="RefreshType">RefreshCommandHandler</command-handler>
<poll-handler>NoPollHandler</poll-handler> <!-- polled by OpFlagsGroup --> <poll-handler>NoPollHandler</poll-handler> <!-- polled by OpFlagsGroup -->
</feature-type> </feature-type>
@ -525,6 +525,34 @@
<poll-handler>NoPollHandler</poll-handler> <!-- polled by OutletStatusGroup --> <poll-handler>NoPollHandler</poll-handler> <!-- polled by OutletStatusGroup -->
</feature-type> </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"> <feature-type name="PowerMeterDataGroup">
<message-dispatcher>PollGroupDispatcher</message-dispatcher> <message-dispatcher>PollGroupDispatcher</message-dispatcher>
<poll-handler ext="0" cmd1="0x82" cmd2="0x00">FlexPollHandler</poll-handler> <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="eventButtonB" group="2">GenericButtonEvent</feature>
<feature name="eventButtonC" group="3">GenericButtonEvent</feature> <feature name="eventButtonC" group="3">GenericButtonEvent</feature>
<feature name="eventButtonD" group="4">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-group name="opFlagsGroup" type="OpFlagsGroup">
<feature name="programLock" bit="0" on="0x00" off="0x01">OpFlags</feature> <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="ledOnOff" bit="1" on="0x02" off="0x03" inverted="true">OpFlags</feature>
<feature name="buttonBeep" bit="2" on="0x04" off="0x05">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="stayAwake" bit="3" on="0x06" off="0x07">OpFlags</feature>
<feature name="buttonConfig">RemoteSceneButtonConfig</feature>
</feature-group> </feature-group>
<default-link name="buttonA" type="controller" group="1" data1="0x03" data2="0x00" data3="0x00"/> <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="buttonB" type="controller" group="2" data1="0x03" data2="0x00" data3="0x00"/>
@ -62,41 +66,66 @@
</device-type> </device-type>
<device-type name="GeneralizedController_MiniRemoteScene8" batteryPowered="true"> <device-type name="GeneralizedController_MiniRemoteScene8" batteryPowered="true">
<feature name="eventButtonA" group="1">GenericButtonEvent</feature> <feature name="eventButtonA" group="2">GenericButtonEvent</feature>
<feature name="eventButtonB" group="2">GenericButtonEvent</feature> <feature name="eventButtonB" group="1">GenericButtonEvent</feature>
<feature name="eventButtonC" group="3">GenericButtonEvent</feature> <feature name="eventButtonC" group="4">GenericButtonEvent</feature>
<feature name="eventButtonD" group="4">GenericButtonEvent</feature> <feature name="eventButtonD" group="3">GenericButtonEvent</feature>
<feature name="eventButtonE" group="5">GenericButtonEvent</feature> <feature name="eventButtonE" group="6">GenericButtonEvent</feature>
<feature name="eventButtonF" group="6">GenericButtonEvent</feature> <feature name="eventButtonF" group="5">GenericButtonEvent</feature>
<feature name="eventButtonG" group="7">GenericButtonEvent</feature> <feature name="eventButtonG" group="8">GenericButtonEvent</feature>
<feature name="eventButtonH" 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-group name="opFlagsGroup" type="OpFlagsGroup">
<feature name="programLock" bit="0" on="0x00" off="0x01">OpFlags</feature> <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="ledOnOff" bit="1" on="0x02" off="0x03" inverted="true">OpFlags</feature>
<feature name="buttonBeep" bit="2" on="0x04" off="0x05">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="stayAwake" bit="3" on="0x06" off="0x07">OpFlags</feature>
<feature name="buttonConfig">RemoteSceneButtonConfig</feature>
</feature-group> </feature-group>
<default-link name="buttonA" type="controller" group="1" 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="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="3" 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="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="5" 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="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="7" 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="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>
<device-type name="GeneralizedController_MiniRemoteSwitch" batteryPowered="true"> <device-type name="GeneralizedController_MiniRemoteSwitch" batteryPowered="true">
<feature name="eventButton">GenericButtonEvent</feature> <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-group name="opFlagsGroup" type="OpFlagsGroup">
<feature name="programLock" bit="0" on="0x00" off="0x01">OpFlags</feature> <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="ledOnOff" bit="1" on="0x02" off="0x03" inverted="true">OpFlags</feature>
<feature name="buttonBeep" bit="2" on="0x04" off="0x05">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="stayAwake" bit="3" on="0x06" off="0x07">OpFlags</feature>
<feature name="buttonConfig">RemoteSwitchButtonConfig</feature>
</feature-group> </feature-group>
<default-link name="button" type="controller" group="1" data1="0x03" data2="0x00" data3="0x00"/> <default-link name="button" type="controller" group="1" data1="0x03" data2="0x00" data3="0x00"/>
</device-type> </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 --> <!-- Dimmable Lighting Control -->
<device-type name="DimmableLightingControl"> <device-type name="DimmableLightingControl">