Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[insteon] Update remote device support #17540

Merged
merged 2 commits into from
Oct 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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";

Expand All @@ -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";
Expand All @@ -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()));
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,11 @@
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;
Expand Down Expand Up @@ -219,49 +220,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;
}

/**
Expand Down Expand Up @@ -494,6 +478,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();
Expand Down Expand Up @@ -599,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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -1845,6 +1848,74 @@ protected Map<Integer, String> getOpFlagCommands(Command cmd) {
}
}

/**
* 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
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -111,24 +112,27 @@ public static List<String> 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) {
Expand Down Expand Up @@ -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<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);
Expand Down Expand Up @@ -401,4 +477,8 @@ public static ThermostatTimeFormat from(String label) throws IllegalArgumentExce
return format;
}
}

public interface DeviceTypeRenamer {
String getNewDeviceType(String deviceType);
}
}
Loading