Skip to content

Commit

Permalink
[insteon] Added the ability to configure devices from the UI (openhab…
Browse files Browse the repository at this point in the history
…#8226)

Signed-off-by: Rob Nielsen <rob.nielsen@yahoo.com>
  • Loading branch information
robnielsen authored and andrewfg committed Aug 31, 2020
1 parent bed71ba commit 3e48b79
Show file tree
Hide file tree
Showing 7 changed files with 152 additions and 14 deletions.
37 changes: 33 additions & 4 deletions bundles/org.openhab.binding.insteon/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ The Insteon device is configured with the following required parameters:
|----------|-------------|
|address|Insteon or X10 address of the device. Insteon device addresses are in the format 'xx.xx.xx', and can be found on the device. X10 device address are in the format 'x.y' and are typically configured on the device.|
|productKey|Insteon binding product key that is used to identy the device. Every Insteon device type is uniquely identified by its Insteon product key, typically a six digit hex number. For some of the older device types (in particular the SwitchLinc switches and dimmers), Insteon does not give a product key, so an arbitrary fake one of the format Fxx.xx.xx (or Xxx.xx.xx for X10 devices) is assigned by the binding.|
|deviceConfig|Optional JSON object with device specific configuration. The JSON object will contain one or more key/value pairs. The key is a parameter for the device and the type of the value will vary.|

The following is a list of the product keys and associated devices.
These have been tested and should work out of the box:
Expand Down Expand Up @@ -409,8 +410,6 @@ Then create entries in the .items file like this:
```

This will give you a contact, the battery level, and the light level.
Note that battery and light level are only updated when either there is motion, light level above/below threshold, tamper switch activated, or the sensor battery runs low.

The motion sensor II includes three additional channels:

**Items**
Expand All @@ -421,6 +420,24 @@ The motion sensor II includes three additional channels:
Number motionSensorTemperatureLevel "motion sensor temperature level" { channel="insteon:device:home:AABBCC:temperatureLevel" }
```

The battery, light level and temperature level are updated when either there is motion, light level above/below threshold, tamper switch activated, or the sensor battery runs low.
This is accomplished by querying the device for the data.
The motion sensor II will also periodically send data if the alternate heartbeat is enabled on the device.

If the alternate heartbeat is enabled, the device can be configured to not query the device and rely on the data from the alternate heartbeat.
Disabling the querying of the device should provide more accurate battery data since it appears to fluctuate with queries of the device.
This can be configured with the device configuration parameter of the device.
The key in the JSON object is `heartbeatOnly` and the value is a boolean:

**Things**

```
Bridge insteon:network:home [port="/dev/ttyUSB0"] {
Thing device AABBCC [address="AA.BB.CC", productKey="F00.00.24", deviceConfig="{'heartbeatOnly': true}"]
}
```

The temperature can be calculated in Fahrenheit using the following formulas:

* If the device is battery powered: `temperature = 0.73 * motionSensorTemperatureLevel - 20.53`
Expand Down Expand Up @@ -724,7 +741,7 @@ Further note that X10 devices are addressed with `houseCode.unitCode`, e.g. `A.2

The binding can command the modem to send broadcasts to a given Insteon group.
Since it is a broadcast message, the corresponding item does *not* take the address of any device, but of the modem itself.
The format is broadcastOnOff#X where X is the group that you want to be able to broadcast messages to:
The format is `broadcastOnOff#X` where X is the group that you want to be able to broadcast messages to:

**Things**

Expand All @@ -746,6 +763,18 @@ Bridge insteon:network:home [port="/dev/ttyUSB0"] {

Flipping this switch to "ON" will cause the modem to send a broadcast message with group=2, and all devices that are configured to respond to it should react.

Channels can also be configured using the device configuration parameter of the device.
The key in the JSON object is `broadcastGroups` and the value is an array of integers:

**Things**

```
Bridge insteon:network:home [port="/dev/ttyUSB0"] {
Thing device AABBCC [address="AA.BB.CC", productKey="0x000045", deviceConfig="{'broadcastGroups': [2]}"]
}
```

## Channel "related" Property

When an Insteon device changes its state because it is directly operated (for example by flipping a switch manually), it sends out a broadcast message to announce the state change, and the binding (if the PLM modem is properly linked as a responder) should update the corresponding openHAB items.
Expand Down Expand Up @@ -773,7 +802,7 @@ A typical example would be a switch configured to broadcast to a group, and one

```
Bridge insteon:network:home [port="/dev/ttyUSB0"] {
Thing device AABBCC [address="A.BB.CC", productKey="0x000045"] {
Thing device AABBCC [address="AA.BB.CC", productKey="0x000045"] {
Channels:
Type switch : broadcastOnOff#3 [related="AA.BB.DD"]
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -249,13 +249,15 @@ public void updateFeatureState(ChannelUID channelUID, State state) {
handler.updateState(channelUID, state);
}

public InsteonDevice makeNewDevice(InsteonAddress addr, String productKey) {
public InsteonDevice makeNewDevice(InsteonAddress addr, String productKey,
Map<String, @Nullable Object> deviceConfigMap) {
DeviceType dt = DeviceTypeLoader.instance().getDeviceType(productKey);
InsteonDevice dev = InsteonDevice.makeDevice(dt);
dev.setAddress(addr);
dev.setProductKey(productKey);
dev.setDriver(driver);
dev.setIsModem(productKey.equals(InsteonDeviceHandler.PLM_PRODUCT_KEY));
dev.setDeviceConfigMap(deviceConfigMap);
if (!dev.hasValidPollingInterval()) {
dev.setPollInterval(devicePollIntervalMilliseconds);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
package org.openhab.binding.insteon.internal.config;

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;

/**
* The {@link InsteonDeviceConfiguration} class contains fields mapping thing configuration parameters.
Expand All @@ -28,11 +29,18 @@ public class InsteonDeviceConfiguration {
// required parameter
private String productKey = "";

// optional parameter
private @Nullable String deviceConfig;

public String getAddress() {
return address;
}

public String getProductKey() {
return productKey;
}

public @Nullable String getDeviceConfig() {
return deviceConfig;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ public static enum DeviceStatus {
private boolean hasModemDBEntry = false;
private DeviceStatus status = DeviceStatus.INITIALIZED;
private Map<Integer, @Nullable GroupMessageStateMachine> groupState = new HashMap<>();
private Map<String, @Nullable Object> deviceConfigMap = new HashMap<String, @Nullable Object>();

/**
* Constructor
Expand Down Expand Up @@ -191,7 +192,15 @@ public void setFeatureQueried(@Nullable DeviceFeature f) {
synchronized (mrequestQueue) {
featureQueried = f;
}
};
}

public void setDeviceConfigMap(Map<String, @Nullable Object> deviceConfigMap) {
this.deviceConfigMap = deviceConfigMap;
}

public Map<String, @Nullable Object> getDeviceConfigMap() {
return deviceConfigMap;
}

public @Nullable DeviceFeature getFeatureQueried() {
synchronized (mrequestQueue) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,20 @@ protected double getDoubleParameter(String key, double def) {
return def;
}

protected boolean getBooleanDeviceConfig(String key, boolean def) {
Object o = feature.getDevice().getDeviceConfigMap().get(key);
if (o != null) {
if (o instanceof Boolean) {
return (Boolean) o;
} else {
logger.warn("{} {}: The value for the '{}' key is not boolean in the device configuration parameter.",
nm(), feature.getDevice().getAddress(), key);
}
}

return def;
}

/**
* Test if message refers to the button configured for given feature
*
Expand Down Expand Up @@ -1017,7 +1031,9 @@ public static class ClosedSleepingContactHandler extends MessageHandler {
public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f) {
feature.publish(OpenClosedType.CLOSED, StateChangeType.ALWAYS);
if (f.getDevice().hasProductKey(InsteonDeviceHandler.MOTION_SENSOR_II_PRODUCT_KEY)) {
sendExtendedQuery(f, (byte) 0x2e, (byte) 03);
if (!getBooleanDeviceConfig("heartbeatOnly", false)) {
sendExtendedQuery(f, (byte) 0x2e, (byte) 03);
}
} else {
sendExtendedQuery(f, (byte) 0x2e, (byte) 00);
}
Expand All @@ -1034,7 +1050,9 @@ public static class OpenedSleepingContactHandler extends MessageHandler {
public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f) {
feature.publish(OpenClosedType.OPEN, StateChangeType.ALWAYS);
if (f.getDevice().hasProductKey(InsteonDeviceHandler.MOTION_SENSOR_II_PRODUCT_KEY)) {
sendExtendedQuery(f, (byte) 0x2e, (byte) 03);
if (!getBooleanDeviceConfig("heartbeatOnly", false)) {
sendExtendedQuery(f, (byte) 0x2e, (byte) 03);
}
} else {
sendExtendedQuery(f, (byte) 0x2e, (byte) 00);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,12 @@
*/
package org.openhab.binding.insteon.internal.handler;

import java.lang.reflect.Type;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
Expand Down Expand Up @@ -43,6 +45,10 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.gson.Gson;
import com.google.gson.JsonParseException;
import com.google.gson.reflect.TypeToken;

/**
* The {@link InsteonDeviceHandler} is responsible for handling commands, which are
* sent to one of the channels.
Expand Down Expand Up @@ -92,6 +98,7 @@ public class InsteonDeviceHandler extends BaseThingHandler {
InsteonBindingConstants.TOP_OUTLET, InsteonBindingConstants.UPDATE, InsteonBindingConstants.WATTS)
.collect(Collectors.toSet()));

public static final String BROADCAST_GROUPS = "broadcastGroups";
public static final String BROADCAST_ON_OFF = "broadcastonoff";
public static final String CMD = "cmd";
public static final String CMD_RESET = "reset";
Expand Down Expand Up @@ -129,6 +136,8 @@ public void initialize() {
scheduler.execute(() -> {
if (getBridge() == null) {
String msg = "An Insteon network bridge has not been selected for this device.";
logger.warn("{} {}", thing.getUID().getAsString(), msg);

updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, msg);
return;
}
Expand All @@ -137,7 +146,7 @@ public void initialize() {
if (!InsteonAddress.isValid(address)) {
String msg = "Unable to start Insteon device, the insteon or X10 address '" + address
+ "' is invalid. It must be in the format 'AB.CD.EF' or 'H.U' (X10).";
logger.warn("{}", msg);
logger.warn("{} {}", thing.getUID().getAsString(), msg);

updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, msg);
return;
Expand All @@ -146,23 +155,41 @@ public void initialize() {
String productKey = config.getProductKey();
if (DeviceTypeLoader.instance().getDeviceType(productKey) == null) {
String msg = "Unable to start Insteon device, invalid product key '" + productKey + "'.";
logger.warn("{}", msg);
logger.warn("{} {}", thing.getUID().getAsString(), msg);

updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, msg);
return;
}

String deviceConfig = config.getDeviceConfig();
Map<String, @Nullable Object> deviceConfigMap;
if (deviceConfig != null) {
Type mapType = new TypeToken<Map<String, Object>>() {
}.getType();
try {
deviceConfigMap = new Gson().fromJson(deviceConfig, mapType);
} catch (JsonParseException e) {
String msg = "The device configuration parameter is not valid JSON.";
logger.warn("{} {}", thing.getUID().getAsString(), msg);

updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, msg);
return;
}
} else {
deviceConfigMap = Collections.emptyMap();
}

InsteonBinding insteonBinding = getInsteonBinding();
InsteonAddress insteonAddress = new InsteonAddress(address);
if (insteonBinding.getDevice(insteonAddress) != null) {
String msg = "a device already exists with the address '" + address + "'.";
logger.warn("{}", msg);
String msg = "A device already exists with the address '" + address + "'.";
logger.warn("{} {}", thing.getUID().getAsString(), msg);

updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, msg);
return;
}

InsteonDevice device = insteonBinding.makeNewDevice(insteonAddress, productKey);
InsteonDevice device = insteonBinding.makeNewDevice(insteonAddress, productKey, deviceConfigMap);

StringBuilder channelList = new StringBuilder();
List<Channel> channels = new ArrayList<>();
Expand Down Expand Up @@ -206,10 +233,50 @@ public void initialize() {
if (f != null) {
if (!f.isFeatureGroup()) {
if (channelId.equals(InsteonBindingConstants.BROADCAST_ON_OFF)) {
Set<String> broadcastChannels = new HashSet<>();
for (Channel channel : thing.getChannels()) {
String id = channel.getUID().getId();
if (id.startsWith(InsteonBindingConstants.BROADCAST_ON_OFF)) {
addChannel(channel, id, channels, channelList);
broadcastChannels.add(id);
}
}

Object groups = deviceConfigMap.get(BROADCAST_GROUPS);
if (groups != null) {
boolean valid = false;
if (groups instanceof List<?>) {
valid = true;
for (Object o : (List<?>) groups) {
if (o instanceof Double && (Double) o % 1 == 0) {
String id = InsteonBindingConstants.BROADCAST_ON_OFF + "#"
+ ((Double) o).intValue();
if (!broadcastChannels.contains(id)) {
ChannelUID channelUID = new ChannelUID(thing.getUID(), id);
ChannelTypeUID channelTypeUID = new ChannelTypeUID(
InsteonBindingConstants.BINDING_ID,
InsteonBindingConstants.SWITCH);
Channel channel = getCallback()
.createChannelBuilder(channelUID, channelTypeUID).withLabel(id)
.build();

addChannel(channel, id, channels, channelList);
broadcastChannels.add(id);
}
} else {
valid = false;
break;
}
}
}

if (!valid) {
String msg = "The value for key " + BROADCAST_GROUPS
+ " must be an array of integers in the device configuration parameter.";
logger.warn("{} {}", thing.getUID().getAsString(), msg);

updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, msg);
return;
}
}
} else {
Expand Down Expand Up @@ -252,7 +319,7 @@ public void initialize() {
String msg = "Product key '" + productKey
+ "' does not have any features that match existing channels.";

logger.warn("{}", msg);
logger.warn("{} {}", thing.getUID().getAsString(), msg);

updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, msg);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,11 @@
</options>
<limitToOptions>false</limitToOptions>
</parameter>

<parameter name="deviceConfig" type="text">
<label>Device Configuration</label>
<description>Optional JSON object with device specific configuration.</description>
</parameter>
</config-description>
</thing-type>

Expand Down

0 comments on commit 3e48b79

Please sign in to comment.