]> git.basschouten.com Git - openhab-addons.git/commitdiff
[insteon] Update remote device support (#17540)
authorJeremy <jsetton@users.noreply.github.com>
Thu, 10 Oct 2024 19:02:52 +0000 (15:02 -0400)
committerGitHub <noreply@github.com>
Thu, 10 Oct 2024 19:02:52 +0000 (21:02 +0200)
* [insteon] Fix remote device not polled when awake

Signed-off-by: jsetton <jeremy.setton@gmail.com>
bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/InsteonBindingConstants.java
bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/device/InsteonDevice.java
bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/device/feature/CommandHandler.java
bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/device/feature/FeatureEnums.java
bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/device/feature/MessageHandler.java
bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/handler/InsteonDeviceHandler.java
bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/transport/message/GroupMessageStateMachine.java
bundles/org.openhab.binding.insteon/src/main/resources/OH-INF/i18n/insteon.properties
bundles/org.openhab.binding.insteon/src/main/resources/device-features.xml
bundles/org.openhab.binding.insteon/src/main/resources/device-types.xml

index 526bb8ddc3f0a072734182951f0fbabbaf0801c0..7394194e79a260c150895ea2c5fb470cc28b8d21 100644 (file)
 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()));
 }
index c5e5de007ef4e9ce617dadb8e15696e73ccc2032..0364ad979b53f998f8f750a65798dfe110a677a2 100644 (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
-     */
-    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);
+     * @param msg the message received
+     * @return true if group or broadcast message is duplicate
+     */
+    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();
@@ -599,9 +590,18 @@ public class InsteonDevice extends BaseDevice<InsteonAddress, InsteonDeviceHandl
     /**
      * Updates this device type
      *
-     * @param newType the new device type to use
+     * @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();
index a67101a79db6ba6ab959d66b44a90701e141b2ab..694178877b06656ab40840ec708305c22fec5738 100644 (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
      */
index 93a68e7fb4ec8033acf34d0add40016b8ba81dc4..df2c788b69a8e73709039ebb336e99a6fcb3de4f 100644 (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);
+    }
 }
index 0cddd0ae67508d341eb5d537631989b05a7b5c06..c83fdb44a766f747ec84b84f5c7c5bcda38ec836 100644 (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
      */
index bd415cf4c5d1593b4158ed826f5528f0c15409bc..429c79baf664f6a87b9b7af2affe214c5a439d35 100644 (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);
index fceb86a53aff6006f83fd5a817def78dd83cb31a..674690e6e62d1220cb1c9e646ce1480e793a4df7 100644 (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() {
-        return duplicate;
-    }
+    /**
+     * 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();
 
-    public byte getLastCommand() {
-        return lastCmd1;
-    }
+        if (cmd1 != lastCmd1 || timestamp != lastTimestamp) {
+            GroupMessageType type = msg.isAllLinkSuccessReport() ? GroupMessageType.SUCCESS
+                    : msg.isAllLinkCleanup() ? GroupMessageType.CLEAN : GroupMessageType.BCAST;
 
-    public long getLastTimestamp() {
-        return lastTimestamp;
+            update(cmd1, timestamp, type);
+        }
+
+        return duplicate;
     }
 
     /**
-     * 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;
+    private void update(byte cmd1, long timestamp, GroupMessageType type) {
+        boolean isNewGroupMsg = cmd1 != lastCmd1 || Math.abs(timestamp - lastTimestamp) > GROUP_STATE_TIMEOUT;
 
-        switch (state) {
-            case EXPECT_BCAST:
-                switch (type) {
-                    case BCAST:
+        switch (type) {
+            case BCAST:
+                switch (state) {
+                    case EXPECT_BCAST:
+                    case EXPECT_SUCCESS:
                         duplicate = false;
                         break;
-                    case CLEAN:
-                    case SUCCESS:
+                    case EXPECT_CLEAN:
                         duplicate = !isNewGroupMsg;
                         break;
                 }
+                state = State.EXPECT_CLEAN;
                 break;
-            case EXPECT_CLEAN:
-                switch (type) {
-                    case BCAST:
+            case CLEAN:
+                switch (state) {
+                    case EXPECT_BCAST:
                         duplicate = !isNewGroupMsg;
                         break;
-                    case CLEAN:
-                    case SUCCESS:
+                    case EXPECT_CLEAN:
+                    case EXPECT_SUCCESS:
                         duplicate = true;
                         break;
                 }
+                state = State.EXPECT_SUCCESS;
                 break;
-            case EXPECT_SUCCESS:
-                switch (type) {
-                    case BCAST:
-                        duplicate = false;
+            case SUCCESS:
+                switch (state) {
+                    case EXPECT_BCAST:
+                        duplicate = !isNewGroupMsg;
                         break;
-                    case CLEAN:
-                    case SUCCESS:
+                    case EXPECT_CLEAN:
+                    case EXPECT_SUCCESS:
                         duplicate = true;
                         break;
                 }
-                break;
-        }
-
-        switch (type) {
-            case BCAST:
-                state = State.EXPECT_CLEAN;
-                break;
-            case CLEAN:
-                state = State.EXPECT_SUCCESS;
-                break;
-            case SUCCESS:
                 state = State.EXPECT_BCAST;
                 break;
         }
 
         lastCmd1 = cmd1;
         lastTimestamp = timestamp;
-
-        logger.debug("{} group:{} type:{} state:{} duplicate:{}", address, group, type, state, duplicate);
-
-        return duplicate;
     }
 }
index 79f1f188b8ce7abf13b691f2293801b9f174a725..50fd41aee42e0a351bed921a278097b696add8bf 100644 (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
index 20b7fc6874e51fdc9d0bbf23dd0a1501993318b3..9a8e5055f69c981071922166f1e32f1445f1babc 100644 (file)
                <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>
                <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>
index a3b283144c9be448c2a774845be18cce185893f9..51ddbdd1eabec20ee176eb9de34f8cf9f912ec88 100644 (file)
                <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"/>
        </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-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"/>
+                       <feature name="buttonConfig">RemoteSceneButtonConfig</feature>
+               </feature-group>
+               <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">