From faf44c4e97b782dcf13812cfed2a3a60e93d2c6a Mon Sep 17 00:00:00 2001 From: jsetton Date: Thu, 10 Oct 2024 10:51:09 -0400 Subject: [PATCH 1/2] [insteon] Fix remote device not polled when awake Signed-off-by: jsetton --- .../internal/device/InsteonDevice.java | 72 ++++++-------- .../device/feature/MessageHandler.java | 40 +------- .../message/GroupMessageStateMachine.java | 95 ++++++++----------- 3 files changed, 78 insertions(+), 129 deletions(-) diff --git a/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/device/InsteonDevice.java b/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/device/InsteonDevice.java index c5e5de007ef4e..ae675c2f320b0 100644 --- a/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/device/InsteonDevice.java +++ b/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/device/InsteonDevice.java @@ -42,8 +42,8 @@ import org.openhab.binding.insteon.internal.device.database.ModemDBRecord; 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 +219,32 @@ public boolean isAwake() { } /** - * 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 +477,13 @@ public void handleMessage(Msg msg) { 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(); diff --git a/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/device/feature/MessageHandler.java b/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/device/feature/MessageHandler.java index 0cddd0ae67508..1d32d0b8e15ce 100644 --- a/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/device/feature/MessageHandler.java +++ b/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/device/feature/MessageHandler.java @@ -52,7 +52,6 @@ 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 boolean canHandle(Msg msg) { * 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 @@ protected boolean matchesFilters(Msg msg) { * @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; } /** @@ -1107,13 +1084,6 @@ public abstract static class SensorMsgHandler extends CustomMsgHandler { @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); } diff --git a/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/transport/message/GroupMessageStateMachine.java b/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/transport/message/GroupMessageStateMachine.java index fceb86a53aff6..674690e6e62d1 100644 --- a/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/transport/message/GroupMessageStateMachine.java +++ b/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/transport/message/GroupMessageStateMachine.java @@ -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 static enum GroupMessageType { * 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; } } From bee21f5db89a120bbd0f933cb5d8c7cc3dd8b732 Mon Sep 17 00:00:00 2001 From: jsetton Date: Thu, 10 Oct 2024 10:51:25 -0400 Subject: [PATCH 2/2] [insteon] Update remote device support Signed-off-by: jsetton --- .../internal/InsteonBindingConstants.java | 17 +-- .../internal/device/InsteonDevice.java | 12 +- .../device/feature/CommandHandler.java | 73 +++++++++++- .../internal/device/feature/FeatureEnums.java | 104 ++++++++++++++++-- .../device/feature/MessageHandler.java | 98 +++++++++++------ .../handler/InsteonDeviceHandler.java | 23 ++-- .../resources/OH-INF/i18n/insteon.properties | 9 ++ .../src/main/resources/device-features.xml | 30 ++++- .../src/main/resources/device-types.xml | 63 ++++++++--- 9 files changed, 341 insertions(+), 88 deletions(-) diff --git a/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/InsteonBindingConstants.java b/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/InsteonBindingConstants.java index 526bb8ddc3f0a..7394194e79a26 100644 --- a/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/InsteonBindingConstants.java +++ b/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/InsteonBindingConstants.java @@ -13,10 +13,13 @@ package org.openhab.binding.insteon.internal; import java.io.File; +import java.util.List; import java.util.Map; import java.util.Set; import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.insteon.internal.device.feature.FeatureEnums.RemoteSceneButtonConfig; +import org.openhab.binding.insteon.internal.device.feature.FeatureEnums.RemoteSwitchButtonConfig; import org.openhab.binding.insteon.internal.device.feature.FeatureEnums.VenstarSystemMode; import org.openhab.core.OpenHAB; import org.openhab.core.thing.ThingTypeUID; @@ -77,7 +80,6 @@ public class InsteonBindingConstants { public static final String FEATURE_RAMP_RATE = "rampRate"; public static final String FEATURE_SCENE_ON_OFF = "sceneOnOff"; public static final String FEATURE_STAY_AWAKE = "stayAwake"; - public static final String FEATURE_SYSTEM_MODE = "systemMode"; public static final String FEATURE_TEMPERATURE_SCALE = "temperatureScale"; public static final String FEATURE_TWO_GROUPS = "2Groups"; @@ -90,6 +92,8 @@ public class InsteonBindingConstants { public static final String FEATURE_TYPE_KEYPAD_BUTTON_ON_MASK = "KeypadButtonOnMask"; public static final String FEATURE_TYPE_KEYPAD_BUTTON_TOGGLE_MODE = "KeypadButtonToggleMode"; public static final String FEATURE_TYPE_OUTLET_SWITCH = "OutletSwitch"; + public static final String FEATURE_TYPE_REMOTE_SCENE_BUTTON_CONFIG = "RemoteSceneButtonConfig"; + public static final String FEATURE_TYPE_REMOTE_SWITCH_BUTTON_CONFIG = "RemoteSwitchButtonConfig"; public static final String FEATURE_TYPE_THERMOSTAT_FAN_MODE = "ThermostatFanMode"; public static final String FEATURE_TYPE_THERMOSTAT_SYSTEM_MODE = "ThermostatSystemMode"; public static final String FEATURE_TYPE_THERMOSTAT_COOL_SETPOINT = "ThermostatCoolSetpoint"; @@ -99,12 +103,9 @@ public class InsteonBindingConstants { public static final String FEATURE_TYPE_VENSTAR_COOL_SETPOINT = "VenstarCoolSetpoint"; public static final String FEATURE_TYPE_VENSTAR_HEAT_SETPOINT = "VenstarHeatSetpoint"; - // List of specific device types - public static final String DEVICE_TYPE_CLIMATE_CONTROL_VENSTAR_THERMOSTAT = "ClimateControl_VenstarThermostat"; - // Map of custom state description options - public static final Map 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> 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())); } diff --git a/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/device/InsteonDevice.java b/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/device/InsteonDevice.java index ae675c2f320b0..0364ad979b53f 100644 --- a/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/device/InsteonDevice.java +++ b/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/device/InsteonDevice.java @@ -40,6 +40,7 @@ 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; @@ -589,9 +590,18 @@ public void updateProductData(ProductData newData) { /** * 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(); diff --git a/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/device/feature/CommandHandler.java b/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/device/feature/CommandHandler.java index a67101a79db6b..694178877b066 100644 --- a/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/device/feature/CommandHandler.java +++ b/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/device/feature/CommandHandler.java @@ -40,6 +40,8 @@ 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 boolean canHandle(Command cmd) { 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 @@ protected Map getOpFlagCommands(Command cmd) { } } + /** + * Remote scene button config command handler + */ + public static class RemoteSceneButtonConfigCommandHandler extends MultiOpFlagsCommandHandler { + RemoteSceneButtonConfigCommandHandler(DeviceFeature feature) { + super(feature); + } + + @Override + protected Map getOpFlagCommands(Command cmd) { + Map 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 getOpFlagCommands(Command cmd) { + Map 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 */ diff --git a/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/device/feature/FeatureEnums.java b/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/device/feature/FeatureEnums.java index 93a68e7fb4ec8..df2c788b69a8e 100644 --- a/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/device/feature/FeatureEnums.java +++ b/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/device/feature/FeatureEnums.java @@ -16,6 +16,7 @@ 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 static List names() { } } - 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 static MicroModuleOpMode valueOf(int value) { } } + 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 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 names() { + return Arrays.stream(values()).map(String::valueOf).toList(); + } + } + public static enum SirenAlertType { CHIME(0x00), LOUD_SIREN(0x01); @@ -401,4 +477,8 @@ public static ThermostatTimeFormat from(String label) throws IllegalArgumentExce return format; } } + + public interface DeviceTypeRenamer { + String getNewDeviceType(String deviceType); + } } diff --git a/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/device/feature/MessageHandler.java b/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/device/feature/MessageHandler.java index 1d32d0b8e15ce..c83fdb44a766f 100644 --- a/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/device/feature/MessageHandler.java +++ b/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/device/feature/MessageHandler.java @@ -33,8 +33,6 @@ 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.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; @@ -970,11 +970,16 @@ public static class OpFlagsReplyHandler extends CustomBitmaskMsgHandler { 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; + } } /** @@ -1020,26 +1025,11 @@ public static class KeypadButtonConfigReplyHandler extends OpFlagsReplyHandler { @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); - } - } - } } /** @@ -1309,19 +1299,14 @@ private int getDuration(int value) { /** * 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 @@ -1334,19 +1319,14 @@ public void handleMessage(byte cmd1, Msg msg) { /** * 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 @@ -1415,6 +1395,52 @@ private int getPower(int power) { } } + /** + * 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 */ diff --git a/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/handler/InsteonDeviceHandler.java b/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/handler/InsteonDeviceHandler.java index bd415cf4c5d15..429c79baf664f 100644 --- a/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/handler/InsteonDeviceHandler.java +++ b/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/handler/InsteonDeviceHandler.java @@ -19,7 +19,6 @@ 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.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 @@ private InsteonDevice createDevice(InsteonAddress address, @Nullable InsteonMode @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 stateDescriptionOptions = CUSTOM_STATE_DESCRIPTION_OPTIONS.get(feature.getType()); if (stateDescriptionOptions == null) { return; } - List options = Stream.of(stateDescriptionOptions).map(value -> new StateOption(value, + List options = stateDescriptionOptions.stream().map(value -> new StateOption(value, StringUtils.capitalizeByWhitespace(value.replace("_", " ").toLowerCase()))).toList(); logger.trace("setting state options for {} to {}", channelUID, options); diff --git a/bundles/org.openhab.binding.insteon/src/main/resources/OH-INF/i18n/insteon.properties b/bundles/org.openhab.binding.insteon/src/main/resources/OH-INF/i18n/insteon.properties index 79f1f188b8ce7..50fd41aee42e0 100644 --- a/bundles/org.openhab.binding.insteon/src/main/resources/OH-INF/i18n/insteon.properties +++ b/bundles/org.openhab.binding.insteon/src/main/resources/OH-INF/i18n/insteon.properties @@ -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 diff --git a/bundles/org.openhab.binding.insteon/src/main/resources/device-features.xml b/bundles/org.openhab.binding.insteon/src/main/resources/device-features.xml index 20b7fc6874e51..9a8e5055f69c9 100644 --- a/bundles/org.openhab.binding.insteon/src/main/resources/device-features.xml +++ b/bundles/org.openhab.binding.insteon/src/main/resources/device-features.xml @@ -235,7 +235,7 @@ DefaultDispatcher IOLincRelayModeReplyHandler NoOpMsgHandler - IOLincRelayModeCommandHandler + IOLincRelayModeCommandHandler RefreshCommandHandler NoPollHandler @@ -525,6 +525,34 @@ NoPollHandler + + DefaultDispatcher + + + CustomDimensionlessMsgHandler + NoOpMsgHandler + RefreshCommandHandler + NoOpCommandHandler + NoPollHandler + + + DefaultDispatcher + RemoteSceneButtonConfigReplyHandler + NoOpMsgHandler + RemoteSceneButtonConfigCommandHandler + RefreshCommandHandler + NoPollHandler + + + DefaultDispatcher + RemoteSwitchButtonConfigReplyHandler + NoOpMsgHandler + RemoteSwitchButtonConfigCommandHandler + RefreshCommandHandler + NoPollHandler + + PollGroupDispatcher FlexPollHandler diff --git a/bundles/org.openhab.binding.insteon/src/main/resources/device-types.xml b/bundles/org.openhab.binding.insteon/src/main/resources/device-types.xml index a3b283144c9be..51ddbdd1eabec 100644 --- a/bundles/org.openhab.binding.insteon/src/main/resources/device-types.xml +++ b/bundles/org.openhab.binding.insteon/src/main/resources/device-types.xml @@ -49,11 +49,15 @@ GenericButtonEvent GenericButtonEvent GenericButtonEvent + + RemoteBatteryLevel + OpFlags OpFlags OpFlags OpFlags + RemoteSceneButtonConfig @@ -62,41 +66,66 @@ - GenericButtonEvent - GenericButtonEvent - GenericButtonEvent - GenericButtonEvent - GenericButtonEvent - GenericButtonEvent - GenericButtonEvent - GenericButtonEvent + GenericButtonEvent + GenericButtonEvent + GenericButtonEvent + GenericButtonEvent + GenericButtonEvent + GenericButtonEvent + GenericButtonEvent + GenericButtonEvent + + RemoteBatteryLevel + OpFlags OpFlags OpFlags OpFlags - - - - - - - - - + RemoteSceneButtonConfig + + + + + + + + + GenericButtonEvent + + RemoteBatteryLevel + OpFlags OpFlags OpFlags OpFlags + RemoteSwitchButtonConfig + + GenericButtonEvent + GenericButtonEvent + + RemoteBatteryLevel + + + OpFlags + OpFlags + OpFlags + OpFlags + RemoteSwitchButtonConfig + + + + +