added migrated 2.x add-ons

Signed-off-by: Kai Kreuzer <kai@openhab.org>
This commit is contained in:
Kai Kreuzer
2020-09-21 01:58:32 +02:00
parent bbf1a7fd29
commit 6df6783b60
11662 changed files with 1302875 additions and 11 deletions

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<features name="org.openhab.binding.insteon-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
<repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
<feature name="openhab-binding-insteon" description="Insteon Binding" version="${project.version}">
<feature>openhab-runtime-base</feature>
<feature>openhab-transport-serial</feature>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.insteon/${project.version}</bundle>
</feature>
</features>

View File

@@ -0,0 +1,549 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.insteon.internal;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ScheduledExecutorService;
import javax.xml.parsers.ParserConfigurationException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.insteon.internal.config.InsteonChannelConfiguration;
import org.openhab.binding.insteon.internal.config.InsteonNetworkConfiguration;
import org.openhab.binding.insteon.internal.device.DeviceFeature;
import org.openhab.binding.insteon.internal.device.DeviceFeatureListener;
import org.openhab.binding.insteon.internal.device.DeviceType;
import org.openhab.binding.insteon.internal.device.DeviceTypeLoader;
import org.openhab.binding.insteon.internal.device.InsteonAddress;
import org.openhab.binding.insteon.internal.device.InsteonDevice;
import org.openhab.binding.insteon.internal.device.InsteonDevice.DeviceStatus;
import org.openhab.binding.insteon.internal.device.RequestQueueManager;
import org.openhab.binding.insteon.internal.driver.Driver;
import org.openhab.binding.insteon.internal.driver.DriverListener;
import org.openhab.binding.insteon.internal.driver.ModemDBEntry;
import org.openhab.binding.insteon.internal.driver.Poller;
import org.openhab.binding.insteon.internal.driver.Port;
import org.openhab.binding.insteon.internal.handler.InsteonDeviceHandler;
import org.openhab.binding.insteon.internal.handler.InsteonNetworkHandler;
import org.openhab.binding.insteon.internal.message.FieldException;
import org.openhab.binding.insteon.internal.message.Msg;
import org.openhab.binding.insteon.internal.message.MsgListener;
import org.openhab.binding.insteon.internal.utils.Utils;
import org.openhab.core.io.transport.serial.SerialPortManager;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.types.Command;
import org.openhab.core.types.State;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xml.sax.SAXException;
/**
* A majority of the code in this file is from the openHAB 1 binding
* org.openhab.binding.insteonplm.InsteonPLMActiveBinding. Including the comments below.
*
* -----------------------------------------------------------------------------------------------
*
* This class represents the actual implementation of the binding, and controls the high level flow
* of messages to and from the InsteonModem.
*
* Writing this binding has been an odyssey through the quirks of the Insteon protocol
* and Insteon devices. A substantial redesign was necessary at some point along the way.
* Here are some of the hard learned lessons that should be considered by anyone who wants
* to re-architect the binding:
*
* 1) The entries of the link database of the modem are not reliable. The category/subcategory entries in
* particular have junk data. Forget about using the modem database to generate a list of devices.
* The database should only be used to verify that a device has been linked.
*
* 2) Querying devices for their product information does not work either. First of all, battery operated devices
* (and there are a lot of those) have their radio switched off, and may generally not respond to product
* queries. Even main stream hardwired devices sold presently (like the 2477s switch and the 2477d dimmer)
* don't even have a product ID. Although supposedly part of the Insteon protocol, we have yet to
* encounter a device that would cough up a product id when queried, even among very recent devices. They
* simply return zeros as product id. Lesson: forget about querying devices to generate a device list.
*
* 3) Polling is a thorny issue: too much traffic on the network, and messages will be dropped left and right,
* and not just the poll related ones, but others as well. In particular sending back-to-back messages
* seemed to result in the second message simply never getting sent, without flow control back pressure
* (NACK) from the modem. For now the work-around is to space out the messages upon sending, and
* in general poll as infrequently as acceptable.
*
* 4) Instantiating and tracking devices when reported by the modem (either from the database, or when
* messages are received) leads to complicated state management because there is no guarantee at what
* point (if at all) the binding configuration will be available. It gets even more difficult when
* items are created, destroyed, and modified while the binding runs.
*
* For the above reasons, devices are only instantiated when they are referenced by binding information.
* As nice as it would be to discover devices and their properties dynamically, we have abandoned that
* path because it had led to a complicated and fragile system which due to the technical limitations
* above was inherently squirrely.
*
*
* @author Bernd Pfrommer - Initial contribution
* @author Daniel Pfrommer - openHAB 1 insteonplm binding
* @author Rob Nielsen - Port to openHAB 2 insteon binding
*/
@NonNullByDefault
@SuppressWarnings({ "null", "unused" })
public class InsteonBinding {
private static final int DEAD_DEVICE_COUNT = 10;
private final Logger logger = LoggerFactory.getLogger(InsteonBinding.class);
private Driver driver;
private ConcurrentHashMap<InsteonAddress, InsteonDevice> devices = new ConcurrentHashMap<>();
private ConcurrentHashMap<String, InsteonChannelConfiguration> bindingConfigs = new ConcurrentHashMap<>();
private PortListener portListener = new PortListener();
private int devicePollIntervalMilliseconds = 300000;
private int deadDeviceTimeout = -1;
private int messagesReceived = 0;
private boolean isActive = false; // state of binding
private int x10HouseUnit = -1;
private InsteonNetworkHandler handler;
public InsteonBinding(InsteonNetworkHandler handler, @Nullable InsteonNetworkConfiguration config,
@Nullable SerialPortManager serialPortManager, ScheduledExecutorService scheduler) {
this.handler = handler;
String port = config.getPort();
logger.debug("port = '{}'", Utils.redactPassword(port));
driver = new Driver(port, portListener, serialPortManager, scheduler);
driver.addMsgListener(portListener);
Integer devicePollIntervalSeconds = config.getDevicePollIntervalSeconds();
if (devicePollIntervalSeconds != null) {
devicePollIntervalMilliseconds = devicePollIntervalSeconds * 1000;
}
logger.debug("device poll interval set to {} seconds", devicePollIntervalMilliseconds / 1000);
String additionalDevices = config.getAdditionalDevices();
if (additionalDevices != null) {
try {
DeviceTypeLoader.instance().loadDeviceTypesXML(additionalDevices);
logger.debug("read additional device definitions from {}", additionalDevices);
} catch (ParserConfigurationException | SAXException | IOException e) {
logger.warn("error reading additional devices from {}", additionalDevices, e);
}
}
String additionalFeatures = config.getAdditionalFeatures();
if (additionalFeatures != null) {
logger.debug("reading additional feature templates from {}", additionalFeatures);
DeviceFeature.readFeatureTemplates(additionalFeatures);
}
deadDeviceTimeout = devicePollIntervalMilliseconds * DEAD_DEVICE_COUNT;
logger.debug("dead device timeout set to {} seconds", deadDeviceTimeout / 1000);
}
public Driver getDriver() {
return driver;
}
public boolean startPolling() {
logger.debug("starting to poll {}", driver.getPortName());
driver.start();
return driver.isRunning();
}
public void setIsActive(boolean isActive) {
this.isActive = isActive;
}
public void sendCommand(String channelName, Command command) {
if (!isActive) {
logger.debug("not ready to handle commands yet, returning.");
return;
}
InsteonChannelConfiguration bindingConfig = bindingConfigs.get(channelName);
if (bindingConfig == null) {
logger.warn("unable to find binding config for channel {}", channelName);
return;
}
InsteonDevice dev = getDevice(bindingConfig.getAddress());
if (dev == null) {
logger.warn("no device found with insteon address {}", bindingConfig.getAddress());
return;
}
dev.processCommand(driver, bindingConfig, command);
logger.debug("found binding config for channel {}", channelName);
}
public void addFeatureListener(InsteonChannelConfiguration bindingConfig) {
logger.debug("adding listener for channel {}", bindingConfig.getChannelName());
InsteonAddress address = bindingConfig.getAddress();
InsteonDevice dev = getDevice(address);
@Nullable
DeviceFeature f = dev.getFeature(bindingConfig.getFeature());
if (f == null || f.isFeatureGroup()) {
StringBuilder buf = new StringBuilder();
ArrayList<String> names = new ArrayList<>(dev.getFeatures().keySet());
Collections.sort(names);
for (String name : names) {
DeviceFeature feature = dev.getFeature(name);
if (!feature.isFeatureGroup()) {
if (buf.length() > 0) {
buf.append(", ");
}
buf.append(name);
}
}
logger.warn("channel {} references unknown feature: {}, it will be ignored. Known features for {} are: {}.",
bindingConfig.getChannelName(), bindingConfig.getFeature(), bindingConfig.getProductKey(),
buf.toString());
return;
}
DeviceFeatureListener fl = new DeviceFeatureListener(this, bindingConfig.getChannelUID(),
bindingConfig.getChannelName());
fl.setParameters(bindingConfig.getParameters());
f.addListener(fl);
bindingConfigs.put(bindingConfig.getChannelName(), bindingConfig);
}
public void removeFeatureListener(ChannelUID channelUID) {
String channelName = channelUID.getAsString();
logger.debug("removing listener for channel {}", channelName);
for (Iterator<Entry<InsteonAddress, InsteonDevice>> it = devices.entrySet().iterator(); it.hasNext();) {
InsteonDevice dev = it.next().getValue();
boolean removedListener = dev.removeFeatureListener(channelName);
if (removedListener) {
logger.trace("removed feature listener {} from dev {}", channelName, dev);
}
}
}
public void updateFeatureState(ChannelUID channelUID, State state) {
handler.updateState(channelUID, state);
}
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);
}
if (driver.isModemDBComplete() && dev.getStatus() != DeviceStatus.POLLING) {
int ndev = checkIfInModemDatabase(dev);
if (dev.hasModemDBEntry()) {
dev.setStatus(DeviceStatus.POLLING);
Poller.instance().startPolling(dev, ndev);
}
}
devices.put(addr, dev);
handler.insteonDeviceWasCreated();
return (dev);
}
public void removeDevice(InsteonAddress addr) {
InsteonDevice dev = devices.remove(addr);
if (dev == null) {
return;
}
if (dev.getStatus() == DeviceStatus.POLLING) {
Poller.instance().stopPolling(dev);
}
}
/**
* Checks if a device is in the modem link database, and, if the database
* is complete, logs a warning if the device is not present
*
* @param dev The device to search for in the modem database
* @return number of devices in modem database
*/
private int checkIfInModemDatabase(InsteonDevice dev) {
try {
InsteonAddress addr = dev.getAddress();
Map<InsteonAddress, @Nullable ModemDBEntry> dbes = driver.lockModemDBEntries();
if (dbes.containsKey(addr)) {
if (!dev.hasModemDBEntry()) {
logger.debug("device {} found in the modem database and {}.", addr, getLinkInfo(dbes, addr, true));
dev.setHasModemDBEntry(true);
}
} else {
if (driver.isModemDBComplete() && !addr.isX10()) {
logger.warn("device {} not found in the modem database. Did you forget to link?", addr);
}
}
return dbes.size();
} finally {
driver.unlockModemDBEntries();
}
}
public Map<String, String> getDatabaseInfo() {
try {
Map<String, String> databaseInfo = new HashMap<>();
Map<InsteonAddress, @Nullable ModemDBEntry> dbes = driver.lockModemDBEntries();
for (InsteonAddress addr : dbes.keySet()) {
String a = addr.toString();
databaseInfo.put(a, a + ": " + getLinkInfo(dbes, addr, false));
}
return databaseInfo;
} finally {
driver.unlockModemDBEntries();
}
}
public boolean reconnect() {
driver.stop();
return startPolling();
}
/**
* Everything below was copied from Insteon PLM v1
*/
/**
* Clean up all state.
*/
public void shutdown() {
logger.debug("shutting down Insteon bridge");
driver.stop();
devices.clear();
RequestQueueManager.destroyInstance();
Poller.instance().stop();
isActive = false;
}
/**
* Method to find a device by address
*
* @param aAddr the insteon address to search for
* @return reference to the device, or null if not found
*/
public @Nullable InsteonDevice getDevice(@Nullable InsteonAddress aAddr) {
InsteonDevice dev = (aAddr == null) ? null : devices.get(aAddr);
return (dev);
}
private String getLinkInfo(Map<InsteonAddress, @Nullable ModemDBEntry> dbes, InsteonAddress a, boolean prefix) {
ModemDBEntry dbe = dbes.get(a);
List<Byte> controls = dbe.getControls();
List<Byte> responds = dbe.getRespondsTo();
Port port = dbe.getPort();
String deviceName = port.getDeviceName();
String s = deviceName.startsWith("/hub") ? "hub" : "plm";
StringBuilder buf = new StringBuilder();
if (port.isModem(a)) {
if (prefix) {
buf.append("it is the ");
}
buf.append(s);
buf.append(" (");
buf.append(Utils.redactPassword(deviceName));
buf.append(")");
} else {
if (prefix) {
buf.append("the ");
}
buf.append(s);
buf.append(" controls groups (");
buf.append(toGroupString(controls));
buf.append(") and responds to groups (");
buf.append(toGroupString(responds));
buf.append(")");
}
return buf.toString();
}
private String toGroupString(List<Byte> group) {
List<Byte> sorted = new ArrayList<>(group);
Collections.sort(sorted, new Comparator<Byte>() {
@Override
public int compare(Byte b1, Byte b2) {
int i1 = b1 & 0xFF;
int i2 = b2 & 0xFF;
return i1 < i2 ? -1 : i1 == i2 ? 0 : 1;
}
});
StringBuilder buf = new StringBuilder();
for (Byte b : sorted) {
if (buf.length() > 0) {
buf.append(",");
}
buf.append(b & 0xFF);
}
return buf.toString();
}
public void logDeviceStatistics() {
String msg = String.format("devices: %3d configured, %3d polling, msgs received: %5d", devices.size(),
Poller.instance().getSizeOfQueue(), messagesReceived);
logger.debug("{}", msg);
messagesReceived = 0;
for (InsteonDevice dev : devices.values()) {
if (dev.isModem()) {
continue;
}
if (deadDeviceTimeout > 0 && dev.getPollOverDueTime() > deadDeviceTimeout) {
logger.debug("device {} has not responded to polls for {} sec", dev.toString(),
dev.getPollOverDueTime() / 3600);
}
}
}
/**
* Handles messages that come in from the ports.
* Will only process one message at a time.
*/
@NonNullByDefault
private class PortListener implements MsgListener, DriverListener {
@Override
public void msg(Msg msg) {
if (msg.isEcho() || msg.isPureNack()) {
return;
}
messagesReceived++;
logger.debug("got msg: {}", msg);
if (msg.isX10()) {
handleX10Message(msg);
} else {
handleInsteonMessage(msg);
}
}
@Override
public void driverCompletelyInitialized() {
List<String> missing = new ArrayList<>();
try {
Map<InsteonAddress, @Nullable ModemDBEntry> dbes = driver.lockModemDBEntries();
logger.debug("modem database has {} entries!", dbes.size());
if (dbes.isEmpty()) {
logger.warn("the modem link database is empty!");
}
for (InsteonAddress k : dbes.keySet()) {
logger.debug("modem db entry: {}", k);
}
Set<InsteonAddress> addrs = new HashSet<>();
for (InsteonDevice dev : devices.values()) {
InsteonAddress a = dev.getAddress();
if (!dbes.containsKey(a)) {
if (!a.isX10()) {
logger.warn("device {} not found in the modem database. Did you forget to link?", a);
}
} else {
if (!dev.hasModemDBEntry()) {
addrs.add(a);
logger.debug("device {} found in the modem database and {}.", a,
getLinkInfo(dbes, a, true));
dev.setHasModemDBEntry(true);
}
if (dev.getStatus() != DeviceStatus.POLLING) {
Poller.instance().startPolling(dev, dbes.size());
}
}
}
for (InsteonAddress k : dbes.keySet()) {
if (!addrs.contains(k)) {
logger.debug("device {} found in the modem database, but is not configured as a thing and {}.",
k, getLinkInfo(dbes, k, true));
missing.add(k.toString());
}
}
} finally {
driver.unlockModemDBEntries();
}
if (!missing.isEmpty()) {
handler.addMissingDevices(missing);
}
}
@Override
public void disconnected() {
handler.bindingDisconnected();
}
private void handleInsteonMessage(Msg msg) {
InsteonAddress toAddr = msg.getAddr("toAddress");
if (!msg.isBroadcast() && !driver.isMsgForUs(toAddr)) {
// not for one of our modems, do not process
return;
}
InsteonAddress fromAddr = msg.getAddr("fromAddress");
if (fromAddr == null) {
logger.debug("invalid fromAddress, ignoring msg {}", msg);
return;
}
handleMessage(fromAddr, msg);
}
private void handleX10Message(Msg msg) {
try {
int x10Flag = msg.getByte("X10Flag") & 0xff;
int rawX10 = msg.getByte("rawX10") & 0xff;
if (x10Flag == 0x80) { // actual command
if (x10HouseUnit != -1) {
InsteonAddress fromAddr = new InsteonAddress((byte) x10HouseUnit);
handleMessage(fromAddr, msg);
}
} else if (x10Flag == 0) {
// what unit the next cmd will apply to
x10HouseUnit = rawX10 & 0xFF;
}
} catch (FieldException e) {
logger.warn("got bad X10 message: {}", msg, e);
return;
}
}
private void handleMessage(InsteonAddress fromAddr, Msg msg) {
InsteonDevice dev = getDevice(fromAddr);
if (dev == null) {
logger.debug("dropping message from unknown device with address {}", fromAddr);
} else {
dev.handleMessage(msg);
}
}
}
}

View File

@@ -0,0 +1,112 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.insteon.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.ThingTypeUID;
/**
* The {@link InsteonBindingConstants} class defines common constants, which are
* used across the whole binding.
*
* @author Rob Nielsen - Initial contribution
*/
@NonNullByDefault
public class InsteonBindingConstants {
public static final String BINDING_ID = "insteon";
// List of all Thing Type UIDs
public static final ThingTypeUID DEVICE_THING_TYPE = new ThingTypeUID(BINDING_ID, "device");
public static final ThingTypeUID NETWORK_THING_TYPE = new ThingTypeUID(BINDING_ID, "network");
// List of all Channel ids
public static final String AC_DELAY = "acDelay";
public static final String BACKLIGHT_DURATION = "backlightDuration";
public static final String BATTERY_LEVEL = "batteryLevel";
public static final String BATTERY_PERCENT = "batteryPercent";
public static final String BATTERY_WATERMARK_LEVEL = "batteryWatermarkLevel";
public static final String BEEP = "beep";
public static final String BOTTOM_OUTLET = "bottomOutlet";
public static final String BUTTON_A = "buttonA";
public static final String BUTTON_B = "buttonB";
public static final String BUTTON_C = "buttonC";
public static final String BUTTON_D = "buttonD";
public static final String BUTTON_E = "buttonE";
public static final String BUTTON_F = "buttonF";
public static final String BUTTON_G = "buttonG";
public static final String BUTTON_H = "buttonH";
public static final String BROADCAST_ON_OFF = "broadcastOnOff";
public static final String CONTACT = "contact";
public static final String COOL_SET_POINT = "coolSetPoint";
public static final String DIMMER = "dimmer";
public static final String FAN = "fan";
public static final String FAN_MODE = "fanMode";
public static final String FAST_ON_OFF = "fastOnOff";
public static final String FAST_ON_OFF_BUTTON_A = "fastOnOffButtonA";
public static final String FAST_ON_OFF_BUTTON_B = "fastOnOffButtonB";
public static final String FAST_ON_OFF_BUTTON_C = "fastOnOffButtonC";
public static final String FAST_ON_OFF_BUTTON_D = "fastOnOffButtonD";
public static final String FAST_ON_OFF_BUTTON_E = "fastOnOffButtonE";
public static final String FAST_ON_OFF_BUTTON_F = "fastOnOffButtonF";
public static final String FAST_ON_OFF_BUTTON_G = "fastOnOffButtonG";
public static final String FAST_ON_OFF_BUTTON_H = "fastOnOffButtonH";
public static final String HEAT_SET_POINT = "heatSetPoint";
public static final String HUMIDITY = "humidity";
public static final String HUMIDITY_HIGH = "humidityHigh";
public static final String HUMIDITY_LOW = "humidityLow";
public static final String IS_COOLING = "isCooling";
public static final String IS_HEATING = "isHeating";
public static final String KEYPAD_BUTTON_A = "keypadButtonA";
public static final String KEYPAD_BUTTON_B = "keypadButtonB";
public static final String KEYPAD_BUTTON_C = "keypadButtonC";
public static final String KEYPAD_BUTTON_D = "keypadButtonD";
public static final String KEYPAD_BUTTON_E = "keypadButtonE";
public static final String KEYPAD_BUTTON_F = "keypadButtonF";
public static final String KEYPAD_BUTTON_G = "keypadButtonG";
public static final String KEYPAD_BUTTON_H = "keypadButtonH";
public static final String KWH = "kWh";
public static final String LAST_HEARD_FROM = "lastHeardFrom";
public static final String LED_BRIGHTNESS = "ledBrightness";
public static final String LED_ONOFF = "ledOnOff";
public static final String LIGHT_DIMMER = "lightDimmer";
public static final String LIGHT_LEVEL = "lightLevel";
public static final String LIGHT_LEVEL_ABOVE_THRESHOLD = "lightLevelAboveThreshold";
public static final String LOAD_DIMMER = "loadDimmer";
public static final String LOAD_SWITCH = "loadSwitch";
public static final String LOAD_SWITCH_FAST_ON_OFF = "loadSwitchFastOnOff";
public static final String LOAD_SWITCH_MANUAL_CHANGE = "loadSwitchManualChange";
public static final String LOWBATTERY = "lowBattery";
public static final String MANUAL_CHANGE = "manualChange";
public static final String MANUAL_CHANGE_BUTTON_A = "manualChangeButtonA";
public static final String MANUAL_CHANGE_BUTTON_B = "manualChangeButtonB";
public static final String MANUAL_CHANGE_BUTTON_C = "manualChangeButtonC";
public static final String MANUAL_CHANGE_BUTTON_D = "manualChangeButtonD";
public static final String MANUAL_CHANGE_BUTTON_E = "manualChangeButtonE";
public static final String MANUAL_CHANGE_BUTTON_F = "manualChangeButtonF";
public static final String MANUAL_CHANGE_BUTTON_G = "manualChangeButtonG";
public static final String MANUAL_CHANGE_BUTTON_H = "manualChangeButtonH";
public static final String NOTIFICATION = "notification";
public static final String ON_LEVEL = "onLevel";
public static final String RAMP_DIMMER = "rampDimmer";
public static final String RAMP_RATE = "rampRate";
public static final String RESET = "reset";
public static final String STAGE1_DURATION = "stage1Duration";
public static final String SWITCH = "switch";
public static final String SYSTEM_MODE = "systemMode";
public static final String TAMPER_SWITCH = "tamperSwitch";
public static final String TEMPERATURE = "temperature";
public static final String TEMPERATURE_LEVEL = "temperatureLevel";
public static final String TOP_OUTLET = "topOutlet";
public static final String UPDATE = "update";
public static final String WATTS = "watts";
}

View File

@@ -0,0 +1,115 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.insteon.internal;
import static org.openhab.binding.insteon.internal.InsteonBindingConstants.*;
import java.util.Collections;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.insteon.internal.discovery.InsteonDeviceDiscoveryService;
import org.openhab.binding.insteon.internal.handler.InsteonDeviceHandler;
import org.openhab.binding.insteon.internal.handler.InsteonNetworkHandler;
import org.openhab.core.config.discovery.DiscoveryService;
import org.openhab.core.io.transport.serial.SerialPortManager;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerFactory;
import org.osgi.framework.ServiceRegistration;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
/**
* The {@link InsteonHandlerFactory} is responsible for creating things and thing
* handlers.
*
* @author Rob Nielsen - Initial contribution
*/
@NonNullByDefault
@Component(configurationPid = "binding.insteon", service = ThingHandlerFactory.class)
public class InsteonHandlerFactory extends BaseThingHandlerFactory {
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Collections
.unmodifiableSet(Stream.of(DEVICE_THING_TYPE, NETWORK_THING_TYPE).collect(Collectors.toSet()));
private final Map<ThingUID, @Nullable ServiceRegistration<?>> discoveryServiceRegs = new HashMap<>();
private final Map<ThingUID, @Nullable ServiceRegistration<?>> serviceRegs = new HashMap<>();
private @Nullable SerialPortManager serialPortManager;
@Reference
protected void setSerialPortManager(final SerialPortManager serialPortManager) {
this.serialPortManager = serialPortManager;
}
protected void unsetSerialPortManager(final SerialPortManager serialPortManager) {
this.serialPortManager = null;
}
@Override
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
}
@Override
protected @Nullable ThingHandler createHandler(Thing thing) {
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
if (NETWORK_THING_TYPE.equals(thingTypeUID)) {
InsteonNetworkHandler insteonNetworkHandler = new InsteonNetworkHandler((Bridge) thing, serialPortManager);
registerServices(insteonNetworkHandler);
return insteonNetworkHandler;
} else if (DEVICE_THING_TYPE.equals(thingTypeUID)) {
return new InsteonDeviceHandler(thing);
}
return null;
}
@Override
protected synchronized void removeHandler(ThingHandler thingHandler) {
if (thingHandler instanceof InsteonNetworkHandler) {
ThingUID uid = thingHandler.getThing().getUID();
ServiceRegistration<?> serviceRegs = this.serviceRegs.remove(uid);
if (serviceRegs != null) {
serviceRegs.unregister();
}
ServiceRegistration<?> discoveryServiceRegs = this.discoveryServiceRegs.remove(uid);
if (discoveryServiceRegs != null) {
discoveryServiceRegs.unregister();
}
}
}
private synchronized void registerServices(InsteonNetworkHandler handler) {
this.serviceRegs.put(handler.getThing().getUID(),
bundleContext.registerService(InsteonNetworkHandler.class.getName(), handler, new Hashtable<>()));
InsteonDeviceDiscoveryService discoveryService = new InsteonDeviceDiscoveryService(handler);
this.discoveryServiceRegs.put(handler.getThing().getUID(),
bundleContext.registerService(DiscoveryService.class.getName(), discoveryService, new Hashtable<>()));
}
}

View File

@@ -0,0 +1,352 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.insteon.internal.command;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.insteon.internal.InsteonBinding;
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.handler.InsteonNetworkHandler;
import org.openhab.binding.insteon.internal.message.FieldException;
import org.openhab.binding.insteon.internal.message.InvalidMessageTypeException;
import org.openhab.binding.insteon.internal.message.Msg;
import org.openhab.binding.insteon.internal.message.MsgListener;
import org.openhab.binding.insteon.internal.utils.Utils;
import org.openhab.core.io.console.Console;
import org.openhab.core.io.console.extensions.AbstractConsoleCommandExtension;
import org.openhab.core.io.console.extensions.ConsoleCommandExtension;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.component.annotations.ReferenceCardinality;
import org.osgi.service.component.annotations.ReferencePolicy;
import org.osgi.service.component.annotations.ReferencePolicyOption;
/**
*
* Console commands for the Insteon binding
*
* @author Rob Nielsen - Initial contribution
*/
@NonNullByDefault
@Component(service = ConsoleCommandExtension.class)
public class InsteonCommandExtension extends AbstractConsoleCommandExtension implements MsgListener {
private static final String DISPLAY_DEVICES = "display_devices";
private static final String DISPLAY_CHANNELS = "display_channels";
private static final String DISPLAY_LOCAL_DATABASE = "display_local_database";
private static final String DISPLAY_MONITORED = "display_monitored";
private static final String START_MONITORING = "start_monitoring";
private static final String STOP_MONITORING = "stop_monitoring";
private static final String SEND_STANDARD_MESSAGE = "send_standard_message";
private static final String SEND_EXTENDED_MESSAGE = "send_extended_message";
private static final String SEND_EXTENDED_MESSAGE_2 = "send_extended_message_2";
private enum MessageType {
STANDARD,
EXTENDED,
EXTENDED_2
};
@Nullable
private InsteonNetworkHandler handler;
@Nullable
private Console console;
private boolean monitoring = false;
private boolean monitorAllDevices = false;
private Set<InsteonAddress> monitoredAddresses = new HashSet<>();
public InsteonCommandExtension() {
super("insteon", "Interact with the Insteon integration.");
}
@Override
public void execute(String[] args, Console console) {
if (args.length > 0) {
InsteonNetworkHandler handler = this.handler; // fix eclipse warnings about nullable
if (handler == null) {
console.println("No Insteon network bridge configured.");
} else {
switch (args[0]) {
case DISPLAY_DEVICES:
if (args.length == 1) {
handler.displayDevices(console);
} else {
printUsage(console);
}
break;
case DISPLAY_CHANNELS:
if (args.length == 1) {
handler.displayChannels(console);
} else {
printUsage(console);
}
break;
case DISPLAY_LOCAL_DATABASE:
if (args.length == 1) {
handler.displayLocalDatabase(console);
} else {
printUsage(console);
}
break;
case DISPLAY_MONITORED:
if (args.length == 1) {
displayMonitoredDevices(console);
} else {
printUsage(console);
}
break;
case START_MONITORING:
if (args.length == 2) {
startMonitoring(console, args[1]);
} else {
printUsage(console);
}
break;
case STOP_MONITORING:
if (args.length == 2) {
stopMonitoring(console, args[1]);
} else {
printUsage(console);
}
break;
case SEND_STANDARD_MESSAGE:
if (args.length == 5) {
sendMessage(console, MessageType.STANDARD, args);
} else {
printUsage(console);
}
break;
case SEND_EXTENDED_MESSAGE:
if (args.length >= 5 && args.length <= 18) {
sendMessage(console, MessageType.EXTENDED, args);
} else {
printUsage(console);
}
break;
case SEND_EXTENDED_MESSAGE_2:
if (args.length >= 5 && args.length <= 17) {
sendMessage(console, MessageType.EXTENDED_2, args);
} else {
printUsage(console);
}
break;
default:
console.println("Unknown command '" + args[0] + "'");
printUsage(console);
break;
}
}
} else {
printUsage(console);
}
}
@Override
public List<String> getUsages() {
return Arrays.asList(new String[] {
buildCommandUsage(DISPLAY_DEVICES, "display devices that are online, along with available channels"),
buildCommandUsage(DISPLAY_CHANNELS,
"display channels that are linked, along with configuration information"),
buildCommandUsage(DISPLAY_LOCAL_DATABASE, "display Insteon PLM or hub database details"),
buildCommandUsage(DISPLAY_MONITORED, "display monitored device(s)"),
buildCommandUsage(START_MONITORING + " all|address",
"start displaying messages received from device(s)"),
buildCommandUsage(STOP_MONITORING + " all|address", "stop displaying messages received from device(s)"),
buildCommandUsage(SEND_STANDARD_MESSAGE + " address flags cmd1 cmd2",
"send standard message to a device"),
buildCommandUsage(SEND_EXTENDED_MESSAGE + " address flags cmd1 cmd2 [up to 13 bytes]",
"send extended message to a device"),
buildCommandUsage(SEND_EXTENDED_MESSAGE_2 + " address flags cmd1 cmd2 [up to 12 bytes]",
"send extended message with a two byte crc to a device") });
}
@Override
public void msg(Msg msg) {
if (monitorAllDevices || monitoredAddresses.contains(msg.getAddr("fromAddress"))) {
String date = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS").format(new Date());
if (console != null) {
console.println(date + " " + msg.toString());
}
}
}
@Reference(cardinality = ReferenceCardinality.OPTIONAL, policy = ReferencePolicy.DYNAMIC, policyOption = ReferencePolicyOption.GREEDY)
public void setInsteonNetworkHandler(InsteonNetworkHandler handler) {
this.handler = handler;
}
public void unsetInsteonNetworkHandler(InsteonNetworkHandler handler) {
this.handler = null;
}
private void displayMonitoredDevices(Console console) {
if (!monitoredAddresses.isEmpty()) {
StringBuilder builder = new StringBuilder();
for (InsteonAddress insteonAddress : monitoredAddresses) {
if (builder.length() == 0) {
builder = new StringBuilder("The individual device(s) ");
} else {
builder.append(", ");
}
builder.append(insteonAddress);
}
console.println(builder.append(" are monitored").toString());
} else if (monitorAllDevices) {
console.println("All devices are monitored.");
} else {
console.println("Not mointoring any devices.");
}
}
private void startMonitoring(Console console, String addr) {
if (addr.equalsIgnoreCase("all")) {
if (monitorAllDevices != true) {
monitorAllDevices = true;
monitoredAddresses.clear();
console.println("Started monitoring all devices.");
} else {
console.println("Already monitoring all devices.");
}
} else {
try {
if (monitorAllDevices) {
console.println("Already monitoring all devices.");
} else if (monitoredAddresses.add(new InsteonAddress(addr))) {
console.println("Started monitoring the device " + addr + ".");
} else {
console.println("Already monitoring the device " + addr + ".");
}
} catch (IllegalArgumentException e) {
console.println("Invalid device address" + addr + ".");
return;
}
}
if (monitoring == false) {
getInsteonBinding().getDriver().addMsgListener(this);
this.console = console;
monitoring = true;
}
}
private void stopMonitoring(Console console, String addr) {
if (monitoring == false) {
console.println("Not mointoring any devices.");
return;
}
if (addr.equalsIgnoreCase("all")) {
if (monitorAllDevices) {
monitorAllDevices = false;
console.println("Stopped monitoring all devices.");
} else {
console.println("Not monitoring all devices.");
}
} else {
try {
if (monitorAllDevices) {
console.println("Not monitoring individual devices.");
} else if (monitoredAddresses.remove(new InsteonAddress(addr))) {
console.println("Stopped monitoring the device " + addr + ".");
} else {
console.println("Not monitoring the device " + addr + ".");
return;
}
} catch (IllegalArgumentException e) {
console.println("Invalid address device address " + addr + ".");
return;
}
}
if (monitorAllDevices == false && monitoredAddresses.isEmpty()) {
getInsteonBinding().getDriver().removeListener(this);
this.console = null;
monitoring = false;
}
}
private void sendMessage(Console console, MessageType messageType, String[] args) {
InsteonDevice device = new InsteonDevice();
device.setDriver(getInsteonBinding().getDriver());
try {
device.setAddress(new InsteonAddress(args[1]));
} catch (IllegalArgumentException e) {
console.println("Invalid device address" + args[1] + ".");
return;
}
StringBuilder builder = new StringBuilder();
for (int i = 2; i < args.length; i++) {
if (!args[i].matches("\\p{XDigit}{1,2}")) {
if (builder.length() > 0) {
builder.append(", ");
}
builder.append(args[i]);
}
}
if (builder.length() != 0) {
builder.append(" is not a valid hexadecimal byte.");
console.print(builder.toString());
return;
}
try {
byte flags = (byte) Utils.fromHexString(args[2]);
byte cmd1 = (byte) Utils.fromHexString(args[3]);
byte cmd2 = (byte) Utils.fromHexString(args[4]);
Msg msg;
if (messageType == MessageType.STANDARD) {
msg = device.makeStandardMessage(flags, cmd1, cmd2);
} else {
byte[] data = new byte[args.length - 5];
for (int i = 0; i + 5 < args.length; i++) {
data[i] = (byte) Utils.fromHexString(args[i + 5]);
}
if (messageType == MessageType.EXTENDED) {
msg = device.makeExtendedMessage(flags, cmd1, cmd2, data);
} else {
msg = device.makeExtendedMessageCRC2(flags, cmd1, cmd2, data);
}
}
device.enqueueMessage(msg, new DeviceFeature(device, "console"));
} catch (FieldException | InvalidMessageTypeException e) {
console.println("Error while trying to create message.");
}
}
@SuppressWarnings("null")
private InsteonBinding getInsteonBinding() {
if (handler == null) {
throw new IllegalArgumentException("No Insteon network bridge configured.");
}
@Nullable
InsteonBinding insteonBinding = handler.getInsteonBinding();
if (insteonBinding == null) {
throw new IllegalArgumentException("Insteon binding is null.");
}
return insteonBinding;
}
}

View File

@@ -0,0 +1,72 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.insteon.internal.config;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.insteon.internal.device.InsteonAddress;
import org.openhab.core.thing.ChannelUID;
/**
*
* This file contains config information needed for each channel
*
* @author Rob Nielsen - Initial contribution
*/
@NonNullByDefault
public class InsteonChannelConfiguration {
private final ChannelUID channelUID;
private final String channelName;
private final InsteonAddress address;
private final String feature;
private final String productKey;
private final Map<String, @Nullable String> parameters;
public InsteonChannelConfiguration(ChannelUID channelUID, String feature, InsteonAddress address, String productKey,
Map<String, @Nullable String> parameters) {
this.channelUID = channelUID;
this.feature = feature;
this.address = address;
this.productKey = productKey;
this.parameters = parameters;
this.channelName = channelUID.getAsString();
}
public ChannelUID getChannelUID() {
return channelUID;
}
public String getChannelName() {
return channelName;
}
public InsteonAddress getAddress() {
return address;
}
public String getFeature() {
return feature;
}
public String getProductKey() {
return productKey;
}
public Map<String, @Nullable String> getParameters() {
return parameters;
}
}

View File

@@ -0,0 +1,46 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
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.
*
* @author Rob Nielsen - Initial contribution
*/
@NonNullByDefault
public class InsteonDeviceConfiguration {
// required parameter
private String address = "";
// 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;
}
}

View File

@@ -0,0 +1,50 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.insteon.internal.config;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* The {@link InsteonNetworkConfiguration} class contains fields mapping thing configuration parameters.
*
* @author Rob Nielsen - Initial contribution
*/
@NonNullByDefault
public class InsteonNetworkConfiguration {
// required parameter
private String port = "";
private @Nullable Integer devicePollIntervalSeconds;
private @Nullable String additionalDevices;
private @Nullable String additionalFeatures;
public String getPort() {
return port;
}
public @Nullable Integer getDevicePollIntervalSeconds() {
return devicePollIntervalSeconds;
}
public @Nullable String getAdditionalDevices() {
return additionalDevices;
}
public @Nullable String getAdditionalFeatures() {
return additionalFeatures;
}
}

View File

@@ -0,0 +1,896 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.insteon.internal.device;
import java.lang.reflect.InvocationTargetException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Timer;
import java.util.TimerTask;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.insteon.internal.config.InsteonChannelConfiguration;
import org.openhab.binding.insteon.internal.device.DeviceFeatureListener.StateChangeType;
import org.openhab.binding.insteon.internal.handler.InsteonDeviceHandler;
import org.openhab.binding.insteon.internal.message.FieldException;
import org.openhab.binding.insteon.internal.message.InvalidMessageTypeException;
import org.openhab.binding.insteon.internal.message.Msg;
import org.openhab.binding.insteon.internal.utils.Utils;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.IncreaseDecreaseType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.PercentType;
import org.openhab.core.types.Command;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A command handler translates an openHAB command into a insteon message
*
* @author Daniel Pfrommer - Initial contribution
* @author Bernd Pfrommer - openHAB 1 insteonplm binding
* @author Rob Nielsen - Port to openHAB 2 insteon binding
*/
@NonNullByDefault
@SuppressWarnings("null")
public abstract class CommandHandler {
private static final Logger logger = LoggerFactory.getLogger(CommandHandler.class);
DeviceFeature feature; // related DeviceFeature
@Nullable
Map<String, @Nullable String> parameters = new HashMap<>();
/**
* Constructor
*
* @param feature The DeviceFeature for which this command was intended.
* The openHAB commands are issued on an openhab item. The .items files bind
* an openHAB item to a DeviceFeature.
*/
CommandHandler(DeviceFeature feature) {
this.feature = feature;
}
/**
* Implements what to do when an openHAB command is received
*
* @param config the configuration for the item that generated the command
* @param cmd the openhab command issued
* @param device the Insteon device to which this command applies
*/
public abstract void handleCommand(InsteonChannelConfiguration conf, Command cmd, InsteonDevice device);
/**
* Returns parameter as integer
*
* @param key key of parameter
* @param def default
* @return value of parameter
*/
protected int getIntParameter(String key, int def) {
String val = parameters.get(key);
if (val == null) {
return (def); // param not found
}
int ret = def;
try {
ret = Utils.strToInt(val);
} catch (NumberFormatException e) {
logger.warn("malformed int parameter in command handler: {}", key);
}
return ret;
}
/**
* Returns parameter as String
*
* @param key key of parameter
* @param def default
* @return value of parameter
*/
protected @Nullable String getStringParameter(String key, String def) {
return (parameters.get(key) == null ? def : parameters.get(key));
}
/**
* Shorthand to return class name for logging purposes
*
* @return name of the class
*/
protected String nm() {
return (this.getClass().getSimpleName());
}
protected int getMaxLightLevel(InsteonChannelConfiguration conf, int defaultLevel) {
Map<String, @Nullable String> params = conf.getParameters();
if (conf.getFeature().contains("dimmer") && params.containsKey("dimmermax")) {
String item = conf.getChannelName();
String dimmerMax = params.get("dimmermax");
try {
int i = Integer.parseInt(dimmerMax);
if (i > 1 && i <= 99) {
int level = (int) Math.ceil((i * 255.0) / 100); // round up
if (level < defaultLevel) {
logger.debug("item {}: using dimmermax value of {}", item, dimmerMax);
return level;
}
} else {
logger.warn("item {}: dimmermax must be between 1-99 inclusive: {}", item, dimmerMax);
}
} catch (NumberFormatException e) {
logger.warn("item {}: invalid int value for dimmermax: {}", item, dimmerMax);
}
}
return defaultLevel;
}
void setParameters(Map<String, @Nullable String> map) {
parameters = map;
}
/**
* Helper function to extract the group parameter from the binding config,
*
* @param c the binding configuration to test
* @return the value of the "group" parameter, or -1 if none
*/
protected static int getGroup(InsteonChannelConfiguration c) {
String v = c.getParameters().get("group");
int iv = -1;
try {
iv = (v == null) ? -1 : Utils.strToInt(v);
} catch (NumberFormatException e) {
logger.warn("malformed int parameter in for item {}", c.getChannelName());
}
return iv;
}
@NonNullByDefault
public static class WarnCommandHandler extends CommandHandler {
WarnCommandHandler(DeviceFeature f) {
super(f);
}
@Override
public void handleCommand(InsteonChannelConfiguration conf, Command cmd, InsteonDevice dev) {
logger.warn("{}: command {} is not implemented yet!", nm(), cmd);
}
}
@NonNullByDefault
public static class NoOpCommandHandler extends CommandHandler {
NoOpCommandHandler(DeviceFeature f) {
super(f);
}
@Override
public void handleCommand(InsteonChannelConfiguration conf, Command cmd, InsteonDevice dev) {
// do nothing, not even log
}
}
@NonNullByDefault
public static class LightOnOffCommandHandler extends CommandHandler {
LightOnOffCommandHandler(DeviceFeature f) {
super(f);
}
@Override
public void handleCommand(InsteonChannelConfiguration conf, Command cmd, InsteonDevice dev) {
try {
int ext = getIntParameter("ext", 0);
int direc = 0x00;
int level = 0x00;
Msg m = null;
if (cmd == OnOffType.ON) {
level = getMaxLightLevel(conf, 0xff);
direc = 0x11;
logger.debug("{}: sent msg to switch {} to {}", nm(), dev.getAddress(),
level == 0xff ? "on" : level);
} else if (cmd == OnOffType.OFF) {
direc = 0x13;
logger.debug("{}: sent msg to switch {} off", nm(), dev.getAddress());
}
if (ext == 1 || ext == 2) {
byte[] data = new byte[] { (byte) getIntParameter("d1", 0), (byte) getIntParameter("d2", 0),
(byte) getIntParameter("d3", 0) };
m = dev.makeExtendedMessage((byte) 0x0f, (byte) direc, (byte) level, data);
logger.debug("{}: was an extended message for device {}", nm(), dev.getAddress());
if (ext == 1) {
m.setCRC();
} else if (ext == 2) {
m.setCRC2();
}
} else {
m = dev.makeStandardMessage((byte) 0x0f, (byte) direc, (byte) level, getGroup(conf));
}
logger.debug("Sending message to {}", dev.getAddress());
dev.enqueueMessage(m, feature);
// expect to get a direct ack after this!
} catch (InvalidMessageTypeException e) {
logger.warn("{}: invalid message: ", nm(), e);
} catch (FieldException e) {
logger.warn("{}: command send message creation error ", nm(), e);
}
}
}
@NonNullByDefault
public static class FastOnOffCommandHandler extends CommandHandler {
FastOnOffCommandHandler(DeviceFeature f) {
super(f);
}
@Override
public void handleCommand(InsteonChannelConfiguration conf, Command cmd, InsteonDevice dev) {
try {
if (cmd == OnOffType.ON) {
int level = getMaxLightLevel(conf, 0xff);
Msg m = dev.makeStandardMessage((byte) 0x0f, (byte) 0x12, (byte) level, getGroup(conf));
dev.enqueueMessage(m, feature);
logger.debug("{}: sent fast on to switch {} level {}", nm(), dev.getAddress(),
level == 0xff ? "on" : level);
} else if (cmd == OnOffType.OFF) {
Msg m = dev.makeStandardMessage((byte) 0x0f, (byte) 0x14, (byte) 0x00, getGroup(conf));
dev.enqueueMessage(m, feature);
logger.debug("{}: sent fast off to switch {}", nm(), dev.getAddress());
}
// expect to get a direct ack after this!
} catch (InvalidMessageTypeException e) {
logger.warn("{}: invalid message: ", nm(), e);
} catch (FieldException e) {
logger.warn("{}: command send message creation error ", nm(), e);
}
}
}
@NonNullByDefault
public static class RampOnOffCommandHandler extends RampCommandHandler {
RampOnOffCommandHandler(DeviceFeature f) {
super(f);
}
@Override
public void handleCommand(InsteonChannelConfiguration conf, Command cmd, InsteonDevice dev) {
try {
if (cmd == OnOffType.ON) {
double ramptime = getRampTime(conf, 0);
int ramplevel = getRampLevel(conf, 100);
byte cmd2 = encode(ramptime, ramplevel);
Msg m = dev.makeStandardMessage((byte) 0x0f, getOnCmd(), cmd2, getGroup(conf));
dev.enqueueMessage(m, feature);
logger.debug("{}: sent ramp on to switch {} time {} level {} cmd1 {}", nm(), dev.getAddress(),
ramptime, ramplevel, getOnCmd());
} else if (cmd == OnOffType.OFF) {
double ramptime = getRampTime(conf, 0);
int ramplevel = getRampLevel(conf, 0 /* ignored */);
byte cmd2 = encode(ramptime, ramplevel);
Msg m = dev.makeStandardMessage((byte) 0x0f, getOffCmd(), cmd2, getGroup(conf));
dev.enqueueMessage(m, feature);
logger.debug("{}: sent ramp off to switch {} time {} cmd1 {}", nm(), dev.getAddress(), ramptime,
getOffCmd());
}
// expect to get a direct ack after this!
} catch (InvalidMessageTypeException e) {
logger.warn("{}: invalid message: ", nm(), e);
} catch (FieldException e) {
logger.warn("{}: command send message creation error ", nm(), e);
}
}
private int getRampLevel(InsteonChannelConfiguration conf, int defaultValue) {
Map<String, @Nullable String> params = conf.getParameters();
return params.containsKey("ramplevel") ? Integer.parseInt(params.get("ramplevel")) : defaultValue;
}
}
@NonNullByDefault
public static class ManualChangeCommandHandler extends CommandHandler {
ManualChangeCommandHandler(DeviceFeature f) {
super(f);
}
@Override
public void handleCommand(InsteonChannelConfiguration conf, Command cmd, InsteonDevice dev) {
try {
if (cmd instanceof DecimalType) {
int v = ((DecimalType) cmd).intValue();
int cmd1 = (v != 1) ? 0x17 : 0x18; // start or stop
int cmd2 = (v == 2) ? 0x01 : 0; // up or down
Msg m = dev.makeStandardMessage((byte) 0x0f, (byte) cmd1, (byte) cmd2, getGroup(conf));
dev.enqueueMessage(m, feature);
logger.debug("{}: cmd {} sent manual change {} {} to {}", nm(), v,
(cmd1 == 0x17) ? "START" : "STOP", (cmd2 == 0x01) ? "UP" : "DOWN", dev.getAddress());
} else {
logger.warn("{}: invalid command type: {}", nm(), cmd);
}
} catch (InvalidMessageTypeException e) {
logger.warn("{}: invalid message: ", nm(), e);
} catch (FieldException e) {
logger.warn("{}: command send message creation error ", nm(), e);
}
}
}
/**
* Sends ALLLink broadcast commands to group
*/
@NonNullByDefault
public static class GroupBroadcastCommandHandler extends CommandHandler {
GroupBroadcastCommandHandler(DeviceFeature f) {
super(f);
}
@Override
public void handleCommand(InsteonChannelConfiguration conf, Command cmd, InsteonDevice dev) {
try {
if (cmd == OnOffType.ON || cmd == OnOffType.OFF) {
byte cmd1 = (byte) ((cmd == OnOffType.ON) ? 0x11 : 0x13);
byte value = (byte) ((cmd == OnOffType.ON) ? 0xFF : 0x00);
int group = getGroup(conf);
if (group == -1) {
logger.warn("no group=xx specified in item {}", conf.getChannelName());
return;
}
logger.debug("{}: sending {} broadcast to group {}", nm(), (cmd1 == 0x11) ? "ON" : "OFF",
getGroup(conf));
Msg m = dev.makeStandardMessage((byte) 0x0f, cmd1, value, group);
dev.enqueueMessage(m, feature);
feature.pollRelatedDevices();
}
} catch (InvalidMessageTypeException e) {
logger.warn("{}: invalid message: ", nm(), e);
} catch (FieldException e) {
logger.warn("{}: command send message creation error ", nm(), e);
}
}
}
@NonNullByDefault
public static class LEDOnOffCommandHandler extends CommandHandler {
LEDOnOffCommandHandler(DeviceFeature f) {
super(f);
}
@Override
public void handleCommand(InsteonChannelConfiguration conf, Command cmd, InsteonDevice dev) {
try {
if (cmd == OnOffType.ON) {
Msg m = dev.makeExtendedMessage((byte) 0x1f, (byte) 0x20, (byte) 0x09,
new byte[] { (byte) 0x00, (byte) 0x00, (byte) 0x00 });
dev.enqueueMessage(m, feature);
logger.debug("{}: sent msg to switch {} on", nm(), dev.getAddress());
} else if (cmd == OnOffType.OFF) {
Msg m = dev.makeExtendedMessage((byte) 0x1f, (byte) 0x20, (byte) 0x08,
new byte[] { (byte) 0x00, (byte) 0x00, (byte) 0x00 });
dev.enqueueMessage(m, feature);
logger.debug("{}: sent msg to switch {} off", nm(), dev.getAddress());
}
} catch (InvalidMessageTypeException e) {
logger.warn("{}: invalid message: ", nm(), e);
} catch (FieldException e) {
logger.warn("{}: command send message creation error ", nm(), e);
}
}
}
@NonNullByDefault
public static class X10OnOffCommandHandler extends CommandHandler {
X10OnOffCommandHandler(DeviceFeature f) {
super(f);
}
@Override
public void handleCommand(InsteonChannelConfiguration conf, Command cmd, InsteonDevice dev) {
try {
byte houseCode = dev.getX10HouseCode();
byte houseUnitCode = (byte) (houseCode << 4 | dev.getX10UnitCode());
if (cmd == OnOffType.ON || cmd == OnOffType.OFF) {
byte houseCommandCode = (byte) (houseCode << 4
| (cmd == OnOffType.ON ? X10.Command.ON.code() : X10.Command.OFF.code()));
Msg munit = dev.makeX10Message(houseUnitCode, (byte) 0x00); // send unit code
dev.enqueueMessage(munit, feature);
Msg mcmd = dev.makeX10Message(houseCommandCode, (byte) 0x80); // send command code
dev.enqueueMessage(mcmd, feature);
String onOff = cmd == OnOffType.ON ? "ON" : "OFF";
logger.debug("{}: sent msg to switch {} {}", nm(), dev.getAddress(), onOff);
}
} catch (InvalidMessageTypeException e) {
logger.warn("{}: invalid message: ", nm(), e);
} catch (FieldException e) {
logger.warn("{}: command send message creation error ", nm(), e);
}
}
}
@NonNullByDefault
public static class X10PercentCommandHandler extends CommandHandler {
X10PercentCommandHandler(DeviceFeature f) {
super(f);
}
@Override
public void handleCommand(InsteonChannelConfiguration conf, Command cmd, InsteonDevice dev) {
try {
//
// I did not have hardware that would respond to the PRESET_DIM codes.
// This code path needs testing.
//
byte houseCode = dev.getX10HouseCode();
byte houseUnitCode = (byte) (houseCode << 4 | dev.getX10UnitCode());
Msg munit = dev.makeX10Message(houseUnitCode, (byte) 0x00); // send unit code
dev.enqueueMessage(munit, feature);
PercentType pc = (PercentType) cmd;
logger.debug("{}: changing level of {} to {}", nm(), dev.getAddress(), pc.intValue());
int level = (pc.intValue() * 32) / 100;
byte cmdCode = (level >= 16) ? X10.Command.PRESET_DIM_2.code() : X10.Command.PRESET_DIM_1.code();
level = level % 16;
if (level <= 0) {
level = 0;
}
houseCode = (byte) x10CodeForLevel[level];
cmdCode |= (houseCode << 4);
Msg mcmd = dev.makeX10Message(cmdCode, (byte) 0x80); // send command code
dev.enqueueMessage(mcmd, feature);
} catch (InvalidMessageTypeException e) {
logger.warn("{}: invalid message: ", nm(), e);
} catch (FieldException e) {
logger.warn("{}: command send message creation error ", nm(), e);
}
}
private final int[] x10CodeForLevel = { 0, 8, 4, 12, 2, 10, 6, 14, 1, 9, 5, 13, 3, 11, 7, 15 };
}
@NonNullByDefault
public static class X10IncreaseDecreaseCommandHandler extends CommandHandler {
X10IncreaseDecreaseCommandHandler(DeviceFeature f) {
super(f);
}
@Override
public void handleCommand(InsteonChannelConfiguration conf, Command cmd, InsteonDevice dev) {
try {
byte houseCode = dev.getX10HouseCode();
byte houseUnitCode = (byte) (houseCode << 4 | dev.getX10UnitCode());
if (cmd == IncreaseDecreaseType.INCREASE || cmd == IncreaseDecreaseType.DECREASE) {
byte houseCommandCode = (byte) (houseCode << 4
| (cmd == IncreaseDecreaseType.INCREASE ? X10.Command.BRIGHT.code()
: X10.Command.DIM.code()));
Msg munit = dev.makeX10Message(houseUnitCode, (byte) 0x00); // send unit code
dev.enqueueMessage(munit, feature);
Msg mcmd = dev.makeX10Message(houseCommandCode, (byte) 0x80); // send command code
dev.enqueueMessage(mcmd, feature);
String bd = cmd == IncreaseDecreaseType.INCREASE ? "BRIGHTEN" : "DIM";
logger.debug("{}: sent msg to switch {} {}", nm(), dev.getAddress(), bd);
}
} catch (InvalidMessageTypeException e) {
logger.warn("{}: invalid message: ", nm(), e);
} catch (FieldException e) {
logger.warn("{}: command send message creation error ", nm(), e);
}
}
}
@NonNullByDefault
public static class IOLincOnOffCommandHandler extends CommandHandler {
IOLincOnOffCommandHandler(DeviceFeature f) {
super(f);
}
@Override
public void handleCommand(InsteonChannelConfiguration conf, Command cmd, InsteonDevice dev) {
try {
if (cmd == OnOffType.ON) {
Msg m = dev.makeStandardMessage((byte) 0x0f, (byte) 0x11, (byte) 0xff);
dev.enqueueMessage(m, feature);
logger.debug("{}: sent msg to switch {} on", nm(), dev.getAddress());
} else if (cmd == OnOffType.OFF) {
Msg m = dev.makeStandardMessage((byte) 0x0f, (byte) 0x13, (byte) 0x00);
dev.enqueueMessage(m, feature);
logger.debug("{}: sent msg to switch {} off", nm(), dev.getAddress());
}
// This used to be configurable, but was made static to make
// the architecture of the binding cleaner.
int delay = 2000;
delay = Math.max(1000, delay);
delay = Math.min(10000, delay);
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
Msg m = feature.makePollMsg();
InsteonDevice dev = feature.getDevice();
if (m != null) {
dev.enqueueMessage(m, feature);
}
}
}, delay);
} catch (InvalidMessageTypeException e) {
logger.warn("{}: invalid message: ", nm(), e);
} catch (FieldException e) {
logger.warn("{}: command send message creation error: ", nm(), e);
}
}
}
@NonNullByDefault
public static class IncreaseDecreaseCommandHandler extends CommandHandler {
IncreaseDecreaseCommandHandler(DeviceFeature f) {
super(f);
}
@Override
public void handleCommand(InsteonChannelConfiguration conf, Command cmd, InsteonDevice dev) {
try {
if (cmd == IncreaseDecreaseType.INCREASE) {
Msg m = dev.makeStandardMessage((byte) 0x0f, (byte) 0x15, (byte) 0x00);
dev.enqueueMessage(m, feature);
logger.debug("{}: sent msg to brighten {}", nm(), dev.getAddress());
} else if (cmd == IncreaseDecreaseType.DECREASE) {
Msg m = dev.makeStandardMessage((byte) 0x0f, (byte) 0x16, (byte) 0x00);
dev.enqueueMessage(m, feature);
logger.debug("{}: sent msg to dimm {}", nm(), dev.getAddress());
}
} catch (InvalidMessageTypeException e) {
logger.warn("{}: invalid message: ", nm(), e);
} catch (FieldException e) {
logger.warn("{}: command send message creation error ", nm(), e);
}
}
}
@NonNullByDefault
public static class PercentHandler extends CommandHandler {
PercentHandler(DeviceFeature f) {
super(f);
}
@Override
public void handleCommand(InsteonChannelConfiguration conf, Command cmd, InsteonDevice dev) {
try {
PercentType pc = (PercentType) cmd;
logger.debug("changing level of {} to {}", dev.getAddress(), pc.intValue());
int level = (int) Math.ceil((pc.intValue() * 255.0) / 100); // round up
if (level > 0) { // make light on message with given level
level = getMaxLightLevel(conf, level);
Msg m = dev.makeStandardMessage((byte) 0x0f, (byte) 0x11, (byte) level);
dev.enqueueMessage(m, feature);
logger.debug("{}: sent msg to set {} to {}", nm(), dev.getAddress(), level);
} else { // switch off
Msg m = dev.makeStandardMessage((byte) 0x0f, (byte) 0x13, (byte) 0x00);
dev.enqueueMessage(m, feature);
logger.debug("{}: sent msg to set {} to zero by switching off", nm(), dev.getAddress());
}
} catch (InvalidMessageTypeException e) {
logger.warn("{}: invalid message: ", nm(), e);
} catch (FieldException e) {
logger.warn("{}: command send message creation error ", nm(), e);
}
}
}
@NonNullByDefault
private abstract static class RampCommandHandler extends CommandHandler {
private static double[] halfRateRampTimes = new double[] { 0.1, 0.3, 2, 6.5, 19, 23.5, 28, 32, 38.5, 47, 90,
150, 210, 270, 360, 480 };
private byte onCmd;
private byte offCmd;
RampCommandHandler(DeviceFeature f) {
super(f);
// Can't process parameters here because they are set after constructor is invoked.
// Unfortunately, this means we can't declare the onCmd, offCmd to be final.
}
@Override
void setParameters(Map<String, @Nullable String> params) {
super.setParameters(params);
onCmd = (byte) getIntParameter("on", 0x2E);
offCmd = (byte) getIntParameter("off", 0x2F);
}
protected final byte getOnCmd() {
return onCmd;
}
protected final byte getOffCmd() {
return offCmd;
}
protected byte encode(double ramptimeSeconds, int ramplevel) throws FieldException {
if (ramplevel < 0 || ramplevel > 100) {
throw new FieldException("ramplevel must be in the range 0-100 (inclusive)");
}
if (ramptimeSeconds < 0) {
throw new FieldException("ramptime must be greater than 0");
}
int ramptime;
int insertionPoint = Arrays.binarySearch(halfRateRampTimes, ramptimeSeconds);
if (insertionPoint > 0) {
ramptime = 15 - insertionPoint;
} else {
insertionPoint = -insertionPoint - 1;
if (insertionPoint == 0) {
ramptime = 15;
} else {
double d1 = Math.abs(halfRateRampTimes[insertionPoint - 1] - ramptimeSeconds);
double d2 = Math.abs(halfRateRampTimes[insertionPoint] - ramptimeSeconds);
ramptime = 15 - (d1 > d2 ? insertionPoint : insertionPoint - 1);
logger.debug("ramp encoding: time {} insert {} d1 {} d2 {} ramp {}", ramptimeSeconds,
insertionPoint, d1, d2, ramptime);
}
}
int r = (int) Math.round(ramplevel / (100.0 / 15.0));
return (byte) (((r & 0x0f) << 4) | (ramptime & 0xf));
}
protected double getRampTime(InsteonChannelConfiguration conf, double defaultValue) {
Map<String, @Nullable String> params = conf.getParameters();
return params.containsKey("ramptime") ? Double.parseDouble(params.get("ramptime")) : defaultValue;
}
}
@NonNullByDefault
public static class RampPercentHandler extends RampCommandHandler {
RampPercentHandler(DeviceFeature f) {
super(f);
}
@Override
public void handleCommand(InsteonChannelConfiguration conf, Command cmd, InsteonDevice dev) {
try {
PercentType pc = (PercentType) cmd;
double ramptime = getRampTime(conf, 0);
int level = pc.intValue();
if (level > 0) { // make light on message with given level
level = getMaxLightLevel(conf, level);
byte cmd2 = encode(ramptime, level);
Msg m = dev.makeStandardMessage((byte) 0x0f, getOnCmd(), cmd2);
dev.enqueueMessage(m, feature);
logger.debug("{}: sent msg to set {} to {} with {} second ramp time.", nm(), dev.getAddress(),
level, ramptime);
} else { // switch off
Msg m = dev.makeStandardMessage((byte) 0x0f, getOffCmd(), (byte) 0x00);
dev.enqueueMessage(m, feature);
logger.debug("{}: sent msg to set {} to zero by switching off with {} ramp time.", nm(),
dev.getAddress(), ramptime);
}
} catch (InvalidMessageTypeException e) {
logger.warn("{}: invalid message: ", nm(), e);
} catch (FieldException e) {
logger.warn("{}: command send message creation error ", nm(), e);
}
}
}
@NonNullByDefault
public static class PowerMeterCommandHandler extends CommandHandler {
PowerMeterCommandHandler(DeviceFeature f) {
super(f);
}
@Override
public void handleCommand(InsteonChannelConfiguration conf, Command cmd, InsteonDevice dev) {
String cmdParam = conf.getParameters().get(InsteonDeviceHandler.CMD);
if (cmdParam == null) {
logger.warn("{} ignoring cmd {} because no cmd= is configured!", nm(), cmd);
return;
}
try {
if (cmd == OnOffType.ON) {
if (cmdParam.equals(InsteonDeviceHandler.CMD_RESET)) {
Msg m = dev.makeStandardMessage((byte) 0x0f, (byte) 0x80, (byte) 0x00);
dev.enqueueMessage(m, feature);
logger.debug("{}: sent reset msg to power meter {}", nm(), dev.getAddress());
feature.publish(OnOffType.OFF, StateChangeType.ALWAYS, InsteonDeviceHandler.CMD,
InsteonDeviceHandler.CMD_RESET);
} else if (cmdParam.equals(InsteonDeviceHandler.CMD_UPDATE)) {
Msg m = dev.makeStandardMessage((byte) 0x0f, (byte) 0x82, (byte) 0x00);
dev.enqueueMessage(m, feature);
logger.debug("{}: sent update msg to power meter {}", nm(), dev.getAddress());
feature.publish(OnOffType.OFF, StateChangeType.ALWAYS, InsteonDeviceHandler.CMD,
InsteonDeviceHandler.CMD_UPDATE);
} else {
logger.warn("{}: ignoring unknown cmd {} for power meter {}", nm(), cmdParam, dev.getAddress());
}
} else if (cmd == OnOffType.OFF) {
logger.debug("{}: ignoring off request for power meter {}", nm(), dev.getAddress());
}
} catch (InvalidMessageTypeException e) {
logger.warn("{}: invalid message: ", nm(), e);
} catch (FieldException e) {
logger.warn("{}: command send message creation error ", nm(), e);
}
}
}
/**
* Command handler that sends a command with a numerical value to a device.
* The handler is very parameterizable so it can be reused for different devices.
* First used for setting thermostat parameters.
*/
@NonNullByDefault
public static class NumberCommandHandler extends CommandHandler {
NumberCommandHandler(DeviceFeature f) {
super(f);
}
public int transform(int cmd) {
return (cmd);
}
@Override
public void handleCommand(InsteonChannelConfiguration conf, Command cmd, InsteonDevice dev) {
try {
int dc = transform(((DecimalType) cmd).intValue());
int intFactor = getIntParameter("factor", 1);
//
// determine what level should be, and what field it should be in
//
int ilevel = dc * intFactor;
byte level = (byte) (ilevel > 255 ? 0xFF : ((ilevel < 0) ? 0 : ilevel));
String vfield = getStringParameter("value", "");
if (vfield == "") {
logger.warn("{} has no value field specified", nm());
}
//
// figure out what cmd1, cmd2, d1, d2, d3 are supposed to be
// to form a proper message
//
int cmd1 = getIntParameter("cmd1", -1);
if (cmd1 < 0) {
logger.warn("{} has no cmd1 specified!", nm());
return;
}
int cmd2 = getIntParameter("cmd2", 0);
int ext = getIntParameter("ext", 0);
Msg m = null;
if (ext == 1 || ext == 2) {
byte[] data = new byte[] { (byte) getIntParameter("d1", 0), (byte) getIntParameter("d2", 0),
(byte) getIntParameter("d3", 0) };
m = dev.makeExtendedMessage((byte) 0x0f, (byte) cmd1, (byte) cmd2, data);
m.setByte(vfield, level);
if (ext == 1) {
m.setCRC();
} else if (ext == 2) {
m.setCRC2();
}
} else {
m = dev.makeStandardMessage((byte) 0x0f, (byte) cmd1, (byte) cmd2);
m.setByte(vfield, level);
}
dev.enqueueMessage(m, feature);
logger.debug("{}: sent msg to change level to {}", nm(), ((DecimalType) cmd).intValue());
m = null;
} catch (InvalidMessageTypeException e) {
logger.warn("{}: invalid message: ", nm(), e);
} catch (FieldException e) {
logger.warn("{}: command send message creation error ", nm(), e);
}
}
}
/**
* Handler to set the thermostat system mode
*/
@NonNullByDefault
public static class ThermostatSystemModeCommandHandler extends NumberCommandHandler {
ThermostatSystemModeCommandHandler(DeviceFeature f) {
super(f);
}
@Override
public int transform(int cmd) {
switch (cmd) {
case 0:
return (0x09); // off
case 1:
return (0x04); // heat
case 2:
return (0x05); // cool
case 3:
return (0x06); // auto (aka manual auto)
case 4:
return (0x0A); // program (aka auto)
default:
break;
}
return (0x0A); // when in doubt go to program
}
}
/**
* Handler to set the thermostat fan mode
*/
@NonNullByDefault
public static class ThermostatFanModeCommandHandler extends NumberCommandHandler {
ThermostatFanModeCommandHandler(DeviceFeature f) {
super(f);
}
@Override
public int transform(int cmd) {
switch (cmd) {
case 0:
return (0x08); // fan mode auto
case 1:
return (0x07); // fan always on
default:
break;
}
return (0x08); // when in doubt go auto mode
}
}
/**
* Handler to set the fanlinc fan mode
*/
@NonNullByDefault
public static class FanLincFanCommandHandler extends NumberCommandHandler {
FanLincFanCommandHandler(DeviceFeature f) {
super(f);
}
@Override
public int transform(int cmd) {
switch (cmd) {
case 0:
return (0x00); // fan off
case 1:
return (0x55); // fan low
case 2:
return (0xAA); // fan medium
case 3:
return (0xFF); // fan high
default:
break;
}
return (0x00); // all other modes are "off"
}
}
/**
* Factory method for creating handlers of a given name using java reflection
*
* @param name the name of the handler to create
* @param params
* @param f the feature for which to create the handler
* @return the handler which was created
*/
@Nullable
public static <T extends CommandHandler> T makeHandler(String name, Map<String, @Nullable String> params,
DeviceFeature f) {
String cname = CommandHandler.class.getName() + "$" + name;
try {
Class<?> c = Class.forName(cname);
@SuppressWarnings("unchecked")
Class<? extends T> dc = (Class<? extends T>) c;
T ch = dc.getDeclaredConstructor(DeviceFeature.class).newInstance(f);
ch.setParameters(params);
return ch;
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException | IllegalArgumentException
| InvocationTargetException | NoSuchMethodException | SecurityException e) {
logger.warn("error trying to create message handler: {}", name, e);
}
return null;
}
}

View File

@@ -0,0 +1,441 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.insteon.internal.device;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.insteon.internal.config.InsteonChannelConfiguration;
import org.openhab.binding.insteon.internal.device.DeviceFeatureListener.StateChangeType;
import org.openhab.binding.insteon.internal.message.Msg;
import org.openhab.binding.insteon.internal.utils.Utils.ParsingException;
import org.openhab.core.types.Command;
import org.openhab.core.types.State;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A DeviceFeature represents a certain feature (trait) of a given Insteon device, e.g. something
* operating under a given InsteonAddress that can be manipulated (relay) or read (sensor).
*
* The DeviceFeature does the processing of incoming messages, and handles commands for the
* particular feature it represents.
*
* It uses four mechanisms for that:
*
* 1) MessageDispatcher: makes high level decisions about an incoming message and then runs the
* 2) MessageHandler: further processes the message, updates state etc
* 3) CommandHandler: translates commands from the openhab bus into an Insteon message.
* 4) PollHandler: creates an Insteon message to query the DeviceFeature
*
* Lastly, DeviceFeatureListeners can register with the DeviceFeature to get notifications when
* the state of a feature has changed. In practice, a DeviceFeatureListener corresponds to an
* openHAB item.
*
* The character of a DeviceFeature is thus given by a set of message and command handlers.
* A FeatureTemplate captures exactly that: it says what set of handlers make up a DeviceFeature.
*
* DeviceFeatures are added to a new device by referencing a FeatureTemplate (defined in device_features.xml)
* from the Device definition file (device_types.xml).
*
* @author Daniel Pfrommer - Initial contribution
* @author Bernd Pfrommer - openHAB 1 insteonplm binding
* @author Rob Nielsen - Port to openHAB 2 insteon binding
*/
@NonNullByDefault
@SuppressWarnings("null")
public class DeviceFeature {
public static enum QueryStatus {
NEVER_QUERIED,
QUERY_PENDING,
QUERY_ANSWERED
}
private static final Logger logger = LoggerFactory.getLogger(DeviceFeature.class);
private static Map<String, FeatureTemplate> features = new HashMap<>();
private InsteonDevice device = new InsteonDevice();
private String name = "INVALID_FEATURE_NAME";
private boolean isStatus = false;
private int directAckTimeout = 6000;
private QueryStatus queryStatus = QueryStatus.NEVER_QUERIED;
private @Nullable MessageHandler defaultMsgHandler = new MessageHandler.DefaultMsgHandler(this);
private @Nullable CommandHandler defaultCommandHandler = new CommandHandler.WarnCommandHandler(this);
private @Nullable PollHandler pollHandler = null;
private @Nullable MessageDispatcher dispatcher = null;
private Map<Integer, @Nullable MessageHandler> msgHandlers = new HashMap<>();
private Map<Class<? extends Command>, @Nullable CommandHandler> commandHandlers = new HashMap<>();
private List<DeviceFeatureListener> listeners = new ArrayList<>();
private List<DeviceFeature> connectedFeatures = new ArrayList<>();
/**
* Constructor
*
* @param device Insteon device to which this feature belongs
* @param name descriptive name for that feature
*/
public DeviceFeature(InsteonDevice device, String name) {
this.name = name;
setDevice(device);
}
/**
* Constructor
*
* @param name descriptive name of the feature
*/
public DeviceFeature(String name) {
this.name = name;
}
// various simple getters
public String getName() {
return name;
}
public synchronized QueryStatus getQueryStatus() {
return queryStatus;
}
public InsteonDevice getDevice() {
return device;
}
public boolean isFeatureGroup() {
return !connectedFeatures.isEmpty();
}
public boolean isStatusFeature() {
return isStatus;
}
public int getDirectAckTimeout() {
return directAckTimeout;
}
public @Nullable MessageHandler getDefaultMsgHandler() {
return defaultMsgHandler;
}
public Map<Integer, @Nullable MessageHandler> getMsgHandlers() {
return this.msgHandlers;
}
public List<DeviceFeature> getConnectedFeatures() {
return (connectedFeatures);
}
// various simple setters
public void setStatusFeature(boolean f) {
isStatus = f;
}
public void setPollHandler(@Nullable PollHandler h) {
pollHandler = h;
}
public void setDevice(InsteonDevice d) {
device = d;
}
public void setMessageDispatcher(@Nullable MessageDispatcher md) {
dispatcher = md;
}
public void setDefaultCommandHandler(@Nullable CommandHandler ch) {
defaultCommandHandler = ch;
}
public void setDefaultMsgHandler(@Nullable MessageHandler mh) {
defaultMsgHandler = mh;
}
public synchronized void setQueryStatus(QueryStatus status) {
logger.trace("{} set query status to: {}", name, status);
queryStatus = status;
}
public void setTimeout(@Nullable String s) {
if (s != null && !s.isEmpty()) {
try {
directAckTimeout = Integer.parseInt(s);
logger.trace("ack timeout set to {}", directAckTimeout);
} catch (NumberFormatException e) {
logger.warn("invalid number for timeout: {}", s);
}
}
}
/**
* Add a listener (item) to a device feature
*
* @param l the listener
*/
public void addListener(DeviceFeatureListener l) {
synchronized (listeners) {
for (DeviceFeatureListener m : listeners) {
if (m.getItemName().equals(l.getItemName())) {
return;
}
}
listeners.add(l);
}
}
/**
* Adds a connected feature such that this DeviceFeature can
* act as a feature group
*
* @param f the device feature related to this feature
*/
public void addConnectedFeature(DeviceFeature f) {
connectedFeatures.add(f);
}
public boolean hasListeners() {
if (!listeners.isEmpty()) {
return true;
}
for (DeviceFeature f : connectedFeatures) {
if (f.hasListeners()) {
return true;
}
}
return false;
}
/**
* removes a DeviceFeatureListener from this feature
*
* @param aItemName name of the item to remove as listener
* @return true if a listener was removed
*/
public boolean removeListener(String aItemName) {
boolean listenerRemoved = false;
synchronized (listeners) {
for (Iterator<DeviceFeatureListener> it = listeners.iterator(); it.hasNext();) {
DeviceFeatureListener fl = it.next();
if (fl.getItemName().equals(aItemName)) {
it.remove();
listenerRemoved = true;
}
}
}
return listenerRemoved;
}
public boolean isReferencedByItem(String aItemName) {
synchronized (listeners) {
for (DeviceFeatureListener fl : listeners) {
if (fl.getItemName().equals(aItemName)) {
return true;
}
}
}
return false;
}
/**
* Called when message is incoming. Dispatches message according to message dispatcher
*
* @param msg The message to dispatch
* @return true if dispatch successful
*/
public boolean handleMessage(Msg msg) {
if (dispatcher == null) {
logger.warn("{} no dispatcher for msg {}", name, msg);
return false;
}
return (dispatcher.dispatch(msg));
}
/**
* Called when an openhab command arrives for this device feature
*
* @param c the binding config of the item which sends the command
* @param cmd the command to be exectued
*/
public void handleCommand(InsteonChannelConfiguration c, Command cmd) {
Class<? extends Command> key = cmd.getClass();
CommandHandler h = commandHandlers.containsKey(key) ? commandHandlers.get(key) : defaultCommandHandler;
logger.trace("{} uses {} to handle command {} for {}", getName(), h.getClass().getSimpleName(),
key.getSimpleName(), getDevice().getAddress());
h.handleCommand(c, cmd, getDevice());
}
/**
* Make a poll message using the configured poll message handler
*
* @return the poll message
*/
public @Nullable Msg makePollMsg() {
if (pollHandler == null) {
return null;
}
logger.trace("{} making poll msg for {} using handler {}", getName(), getDevice().getAddress(),
pollHandler.getClass().getSimpleName());
Msg m = pollHandler.makeMsg(device);
return m;
}
/**
* Publish new state to all device feature listeners, but give them
* additional dataKey and dataValue information so they can decide
* whether to publish the data to the bus.
*
* @param newState state to be published
* @param changeType what kind of changes to publish
* @param dataKey the key on which to filter
* @param dataValue the value that must be matched
*/
public void publish(State newState, StateChangeType changeType, String dataKey, String dataValue) {
logger.debug("{}:{} publishing: {}", this.getDevice().getAddress(), getName(), newState);
synchronized (listeners) {
for (DeviceFeatureListener listener : listeners) {
listener.stateChanged(newState, changeType, dataKey, dataValue);
}
}
}
/**
* Publish new state to all device feature listeners
*
* @param newState state to be published
* @param changeType what kind of changes to publish
*/
public void publish(State newState, StateChangeType changeType) {
logger.debug("{}:{} publishing: {}", this.getDevice().getAddress(), getName(), newState);
synchronized (listeners) {
for (DeviceFeatureListener listener : listeners) {
listener.stateChanged(newState, changeType);
}
}
}
/**
* Poll all device feature listeners for related devices
*/
public void pollRelatedDevices() {
synchronized (listeners) {
for (DeviceFeatureListener listener : listeners) {
listener.pollRelatedDevices();
}
}
}
/**
* Adds a message handler to this device feature.
*
* @param cm1 The insteon cmd1 of the incoming message for which the handler should be used
* @param handler the handler to invoke
*/
public void addMessageHandler(int cm1, @Nullable MessageHandler handler) {
synchronized (msgHandlers) {
msgHandlers.put(cm1, handler);
}
}
/**
* Adds a command handler to this device feature
*
* @param c the command for which this handler is invoked
* @param handler the handler to call
*/
public void addCommandHandler(Class<? extends Command> c, @Nullable CommandHandler handler) {
synchronized (commandHandlers) {
commandHandlers.put(c, handler);
}
}
/**
* Turn DeviceFeature into String
*/
@Override
public String toString() {
return name + "(" + listeners.size() + ":" + commandHandlers.size() + ":" + msgHandlers.size() + ")";
}
/**
* Factory method for creating DeviceFeatures.
*
* @param s The name of the device feature to create.
* @return The newly created DeviceFeature, or null if requested DeviceFeature does not exist.
*/
@Nullable
public static DeviceFeature makeDeviceFeature(String s) {
DeviceFeature f = null;
synchronized (features) {
if (features.containsKey(s)) {
f = features.get(s).build();
} else {
logger.warn("unimplemented feature requested: {}", s);
}
}
return f;
}
/**
* Reads the features templates from an input stream and puts them in global map
*
* @param input the input stream from which to read the feature templates
*/
public static void readFeatureTemplates(InputStream input) {
try {
List<FeatureTemplate> featureTemplates = FeatureTemplateLoader.readTemplates(input);
synchronized (features) {
for (FeatureTemplate f : featureTemplates) {
features.put(f.getName(), f);
}
}
} catch (IOException e) {
logger.warn("IOException while reading device features", e);
} catch (ParsingException e) {
logger.warn("Parsing exception while reading device features", e);
}
}
/**
* Reads the feature templates from a file and adds them to a global map
*
* @param file name of the file to read from
*/
public static void readFeatureTemplates(String file) {
try {
FileInputStream fis = new FileInputStream(file);
readFeatureTemplates(fis);
} catch (FileNotFoundException e) {
logger.warn("cannot read feature templates from file {} ", file, e);
}
}
/**
* static initializer
*/
static {
// read features from xml file and store them in a map
InputStream input = DeviceFeature.class.getResourceAsStream("/device_features.xml");
readFeatureTemplates(input);
}
}

View File

@@ -0,0 +1,192 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.insteon.internal.device;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.insteon.internal.InsteonBinding;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.PercentType;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.types.State;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A DeviceFeatureListener essentially represents an openHAB item that
* listens to a particular feature of an Insteon device
*
* @author Daniel Pfrommer - Initial contribution
* @author Bernd Pfrommer - openHAB 1 insteonplm binding
* @author Rob Nielsen - Port to openHAB 2 insteon binding
*/
@NonNullByDefault
public class DeviceFeatureListener {
private final Logger logger = LoggerFactory.getLogger(DeviceFeatureListener.class);
public enum StateChangeType {
ALWAYS,
CHANGED
};
private String itemName;
private ChannelUID channelUID;
private Map<String, @Nullable String> parameters = new HashMap<>();
private Map<Class<?>, @Nullable State> state = new HashMap<>();
private List<InsteonAddress> relatedDevices = new ArrayList<>();
private InsteonBinding binding;
private static final int TIME_DELAY_POLL_RELATED_MSEC = 5000;
/**
* Constructor
*
* @param item name of the item that is listening
* @param channelUID channel associated with this item
* @param eventPublisher the publisher to use for publishing on the openhab bus
*/
public DeviceFeatureListener(InsteonBinding binding, ChannelUID channelUID, String item) {
this.binding = binding;
this.itemName = item;
this.channelUID = channelUID;
}
/**
* Gets item name
*
* @return item name
*/
public String getItemName() {
return itemName;
}
/**
* Test if string parameter is present and has a given value
*
* @param key key to match
* @param value value to match
* @return true if key exists and value matches
*/
private boolean parameterHasValue(String key, String value) {
String v = parameters.get(key);
return (v != null && v.equals(value));
}
/**
* Set parameters for this feature listener
*
* @param p the parameters to set
*/
public void setParameters(Map<String, @Nullable String> p) {
parameters = p;
updateRelatedDevices();
}
/**
* Publishes a state change on the openhab bus
*
* @param newState the new state to publish on the openhab bus
* @param changeType whether to always publish or not
*/
public void stateChanged(State newState, StateChangeType changeType) {
State oldState = state.get(newState.getClass());
if (oldState == null) {
logger.trace("new state: {}:{}", newState.getClass().getSimpleName(), newState);
// state has changed, must publish
publishState(newState);
} else {
logger.trace("old state: {}:{}=?{}", newState.getClass().getSimpleName(), oldState, newState);
// only publish if state has changed or it is requested explicitly
if (changeType == StateChangeType.ALWAYS || !oldState.equals(newState)) {
publishState(newState);
}
}
state.put(newState.getClass(), newState);
}
/**
* Call this function to inform about a state change for a given
* parameter key and value. If dataKey and dataValue don't match,
* the state change will be ignored.
*
* @param state the new state to which the feature has changed
* @param changeType how to process the state change (always, or only when changed)
* @param dataKey the data key on which to filter
* @param dataValue the value that the data key must match for the state to be published
*/
public void stateChanged(State state, StateChangeType changeType, String dataKey, String dataValue) {
if (parameterHasValue(dataKey, dataValue)) {
stateChanged(state, changeType);
}
}
/**
* Publish the state. In the case of PercentType, if the value is
* 0, send a OnOffType.OFF and if the value is 100, send a OnOffType.ON.
* That way an openHAB Switch will work properly with a Insteon dimmer,
* as long it is used like a switch (On/Off). An openHAB DimmerItem will
* internally convert the ON back to 100% and OFF back to 0, so there is
* no need to send both 0/OFF and 100/ON.
*
* @param state the new state of the feature
*/
private void publishState(State state) {
State publishState = state;
if (state instanceof PercentType) {
if (state.equals(PercentType.ZERO)) {
publishState = OnOffType.OFF;
} else if (state.equals(PercentType.HUNDRED)) {
publishState = OnOffType.ON;
}
}
pollRelatedDevices();
binding.updateFeatureState(channelUID, publishState);
}
/**
* Extracts related devices from the parameter list and
* stores them for faster access later.
*/
private void updateRelatedDevices() {
String d = parameters.get("related");
if (d == null) {
return;
}
String[] devs = d.split("\\+");
for (String dev : devs) {
InsteonAddress a = InsteonAddress.parseAddress(dev);
relatedDevices.add(a);
}
}
/**
* polls all devices that are related to this item
* by the "related" keyword
*/
public void pollRelatedDevices() {
for (InsteonAddress a : relatedDevices) {
logger.debug("polling related device {} in {} ms", a, TIME_DELAY_POLL_RELATED_MSEC);
InsteonDevice d = binding.getDevice(a);
if (d != null) {
d.doPoll(TIME_DELAY_POLL_RELATED_MSEC);
} else {
logger.warn("device {} related to item {} is not configured!", a, itemName);
}
}
}
}

View File

@@ -0,0 +1,166 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.insteon.internal.device;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map.Entry;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The DeviceType class holds device type definitions that are read from
* an xml file.
*
* @author Bernd Pfrommer - Initial contribution
* @author Rob Nielsen - Port to openHAB 2 insteon binding
*/
@NonNullByDefault
@SuppressWarnings("null")
public class DeviceType {
private String productKey;
private String model = "";
private String description = "";
private HashMap<String, String> features = new HashMap<>();
private HashMap<String, FeatureGroup> featureGroups = new HashMap<>();
/**
* Constructor
*
* @param aProductKey the product key for this device type
*/
public DeviceType(String aProductKey) {
productKey = aProductKey;
}
/**
* Get supported features
*
* @return all features that this device type supports
*/
public HashMap<String, String> getFeatures() {
return features;
}
/**
* Get all feature groups
*
* @return all feature groups of this device type
*/
public HashMap<String, FeatureGroup> getFeatureGroups() {
return featureGroups;
}
/**
* Sets the descriptive model string
*
* @param aModel descriptive model string
*/
public void setModel(String aModel) {
model = aModel;
}
/**
* Sets free text description
*
* @param aDesc free text description
*/
public void setDescription(String aDesc) {
description = aDesc;
}
/**
* Adds feature to this device type
*
* @param aKey the key (e.g. "switch") under which this feature can be referenced in the item binding config
* @param aFeatureName the name (e.g. "GenericSwitch") under which the feature has been defined
* @return false if feature was already there
*/
public boolean addFeature(String aKey, String aFeatureName) {
if (features.containsKey(aKey)) {
return false;
}
features.put(aKey, aFeatureName);
return true;
}
/**
* Adds feature group to device type
*
* @param aKey name of the feature group, which acts as key for lookup later
* @param fg feature group to add
* @return true if add succeeded, false if group was already there
*/
public boolean addFeatureGroup(String aKey, FeatureGroup fg) {
if (features.containsKey(aKey)) {
return false;
}
featureGroups.put(aKey, fg);
return true;
}
@Override
public String toString() {
String s = "pk:" + productKey + "|model:" + model + "|desc:" + description + "|features";
for (Entry<String, String> f : features.entrySet()) {
s += ":" + f.getKey() + "=" + f.getValue();
}
s += "|groups";
for (Entry<String, FeatureGroup> f : featureGroups.entrySet()) {
s += ":" + f.getKey() + "=" + f.getValue();
}
return s;
}
/**
* Class that reflects feature group association
*
* @author Bernd Pfrommer - Initial contribution
*/
@NonNullByDefault
public static class FeatureGroup {
private String name;
private String type;
private ArrayList<String> fgFeatures = new ArrayList<>();
FeatureGroup(String name, String type) {
this.name = name;
this.type = type;
}
public void addFeature(String f) {
fgFeatures.add(f);
}
public ArrayList<String> getFeatures() {
return fgFeatures;
}
public String getName() {
return name;
}
public String getType() {
return type;
}
@Override
public String toString() {
String s = "";
for (String g : fgFeatures) {
s += g + ",";
}
return (s.replaceAll(",$", ""));
}
}
}

View File

@@ -0,0 +1,222 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.insteon.internal.device;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map.Entry;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.insteon.internal.device.DeviceType.FeatureGroup;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
/**
* Reads the device types from an xml file.
*
* @author Daniel Pfrommer - Initial contribution
* @author Bernd Pfrommer - openHAB 1 insteonplm binding
* @author Rob Nielsen - Port to openHAB 2 insteon binding
*/
@NonNullByDefault
@SuppressWarnings("null")
public class DeviceTypeLoader {
private static final Logger logger = LoggerFactory.getLogger(DeviceTypeLoader.class);
private HashMap<String, DeviceType> deviceTypes = new HashMap<>();
private @Nullable static DeviceTypeLoader deviceTypeLoader = null;
private DeviceTypeLoader() {
} // private so nobody can call it
/**
* Finds the device type for a given product key
*
* @param aProdKey product key to search for
* @return the device type, or null if not found
*/
public @Nullable DeviceType getDeviceType(String aProdKey) {
return (deviceTypes.get(aProdKey));
}
/**
* Must call loadDeviceTypesXML() before calling this function!
*
* @return currently known device types
*/
public HashMap<String, DeviceType> getDeviceTypes() {
return (deviceTypes);
}
/**
* Reads the device types from input stream and stores them in memory for
* later access.
*
* @param is the input stream from which to read
*/
public void loadDeviceTypesXML(InputStream in) throws ParserConfigurationException, SAXException, IOException {
DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder dBuilder = dbFactory.newDocumentBuilder();
Document doc = dBuilder.parse(in);
doc.getDocumentElement().normalize();
Node root = doc.getDocumentElement();
NodeList nodes = root.getChildNodes();
for (int i = 0; i < nodes.getLength(); i++) {
Node node = nodes.item(i);
if (node.getNodeType() == Node.ELEMENT_NODE && node.getNodeName().equals("device")) {
processDevice((Element) node);
}
}
}
/**
* Reads the device types from file and stores them in memory for later access.
*
* @param aFileName The name of the file to read from
* @throws ParserConfigurationException
* @throws SAXException
* @throws IOException
*/
public void loadDeviceTypesXML(String aFileName) throws ParserConfigurationException, SAXException, IOException {
File file = new File(aFileName);
InputStream in = new FileInputStream(file);
loadDeviceTypesXML(in);
}
/**
* Process device node
*
* @param e name of the element to process
* @throws SAXException
*/
private void processDevice(Element e) throws SAXException {
String productKey = e.getAttribute("productKey");
if (productKey.equals("")) {
throw new SAXException("device in device_types file has no product key!");
}
if (deviceTypes.containsKey(productKey)) {
logger.warn("overwriting previous definition of device {}", productKey);
deviceTypes.remove(productKey);
}
DeviceType devType = new DeviceType(productKey);
NodeList nodes = e.getChildNodes();
for (int i = 0; i < nodes.getLength(); i++) {
Node node = nodes.item(i);
if (node.getNodeType() != Node.ELEMENT_NODE) {
continue;
}
Element subElement = (Element) node;
if (subElement.getNodeName().equals("model")) {
devType.setModel(subElement.getTextContent());
} else if (subElement.getNodeName().equals("description")) {
devType.setDescription(subElement.getTextContent());
} else if (subElement.getNodeName().equals("feature")) {
processFeature(devType, subElement);
} else if (subElement.getNodeName().equals("feature_group")) {
processFeatureGroup(devType, subElement);
}
deviceTypes.put(productKey, devType);
}
}
private String processFeature(DeviceType devType, Element e) throws SAXException {
String name = e.getAttribute("name");
if (name.equals("")) {
throw new SAXException("feature " + e.getNodeName() + " has feature without name!");
}
if (!name.equals(name.toLowerCase())) {
throw new SAXException("feature name '" + name + "' must be lower case");
}
if (!devType.addFeature(name, e.getTextContent())) {
throw new SAXException("duplicate feature: " + name);
}
return (name);
}
private String processFeatureGroup(DeviceType devType, Element e) throws SAXException {
String name = e.getAttribute("name");
if (name.equals("")) {
throw new SAXException("feature group " + e.getNodeName() + " has no name attr!");
}
String type = e.getAttribute("type");
if (type.equals("")) {
throw new SAXException("feature group " + e.getNodeName() + " has no type attr!");
}
FeatureGroup fg = new FeatureGroup(name, type);
NodeList nodes = e.getChildNodes();
for (int i = 0; i < nodes.getLength(); i++) {
Node node = nodes.item(i);
if (node.getNodeType() != Node.ELEMENT_NODE) {
continue;
}
Element subElement = (Element) node;
if (subElement.getNodeName().equals("feature")) {
fg.addFeature(processFeature(devType, subElement));
} else if (subElement.getNodeName().equals("feature_group")) {
fg.addFeature(processFeatureGroup(devType, subElement));
}
}
if (!devType.addFeatureGroup(name, fg)) {
throw new SAXException("duplicate feature group " + name);
}
return (name);
}
/**
* Helper function for debugging
*/
private void logDeviceTypes() {
for (Entry<String, DeviceType> dt : getDeviceTypes().entrySet()) {
String msg = String.format("%-10s->", dt.getKey()) + dt.getValue();
logger.debug("{}", msg);
}
}
/**
* Singleton instance function, creates DeviceTypeLoader
*
* @return DeviceTypeLoader singleton reference
*/
@Nullable
public static synchronized DeviceTypeLoader instance() {
if (deviceTypeLoader == null) {
deviceTypeLoader = new DeviceTypeLoader();
InputStream input = DeviceTypeLoader.class.getResourceAsStream("/device_types.xml");
try {
deviceTypeLoader.loadDeviceTypesXML(input);
} catch (ParserConfigurationException e) {
logger.warn("parser config error when reading device types xml file: ", e);
} catch (SAXException e) {
logger.warn("SAX exception when reading device types xml file: ", e);
} catch (IOException e) {
logger.warn("I/O exception when reading device types xml file: ", e);
}
logger.debug("loaded {} devices: ", deviceTypeLoader.getDeviceTypes().size());
deviceTypeLoader.logDeviceTypes();
}
return deviceTypeLoader;
}
}

View File

@@ -0,0 +1,173 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.insteon.internal.device;
import java.util.HashMap;
import java.util.Map.Entry;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.types.Command;
/**
* A simple class which contains the basic info needed to create a device feature.
* Here, all handlers are represented as strings. The actual device feature
* is then instantiated from the template by calling the build() function.
*
* @author Daniel Pfrommer - Initial contribution
* @author Rob Nielsen - Port to openHAB 2 insteon binding
*/
@NonNullByDefault
@SuppressWarnings("null")
public class FeatureTemplate {
private String name;
private String timeout;
private boolean isStatus;
private @Nullable HandlerEntry dispatcher = null;
private @Nullable HandlerEntry pollHandler = null;
private @Nullable HandlerEntry defaultMsgHandler = null;
private @Nullable HandlerEntry defaultCmdHandler = null;
private HashMap<Integer, HandlerEntry> messageHandlers = new HashMap<>();
private HashMap<Class<? extends Command>, HandlerEntry> commandHandlers = new HashMap<>();
public FeatureTemplate(String name, boolean isStatus, String timeout) {
this.name = name;
this.isStatus = isStatus;
this.timeout = timeout;
}
// simple getters
public String getName() {
return name;
}
public String getTimeout() {
return timeout;
}
public boolean isStatusFeature() {
return isStatus;
}
public @Nullable HandlerEntry getPollHandler() {
return pollHandler;
}
public @Nullable HandlerEntry getDispatcher() {
return dispatcher;
}
public @Nullable HandlerEntry getDefaultCommandHandler() {
return defaultCmdHandler;
}
public @Nullable HandlerEntry getDefaultMessageHandler() {
return defaultMsgHandler;
}
/**
* Retrieves a hashmap of message command code to command handler name
*
* @return a Hashmap from Integer to String representing the command codes and the associated message handlers
*/
public HashMap<Integer, HandlerEntry> getMessageHandlers() {
return messageHandlers;
}
/**
* Similar to getMessageHandlers(), but for command handlers
* Instead of Integers it uses the class of the Command as a key
*
* @see #getMessageHandlers()
* @return a HashMap from Command Classes to CommandHandler names
*/
public HashMap<Class<? extends Command>, HandlerEntry> getCommandHandlers() {
return commandHandlers;
}
// simple setters
public void setMessageDispatcher(HandlerEntry he) {
dispatcher = he;
}
public void setPollHandler(HandlerEntry he) {
pollHandler = he;
}
public void setDefaultCommandHandler(HandlerEntry cmd) {
defaultCmdHandler = cmd;
}
public void setDefaultMessageHandler(HandlerEntry he) {
defaultMsgHandler = he;
}
/**
* Adds a message handler mapped from the command which this handler should be invoked for
* to the name of the handler to be created
*
* @param cmd command to be mapped
* @param he handler entry to map to
*/
public void addMessageHandler(int cmd, HandlerEntry he) {
messageHandlers.put(cmd, he);
}
/**
* Adds a command handler mapped from the command class which this handler should be invoke for
* to the name of the handler to be created
*/
public void addCommandHandler(Class<? extends Command> command, HandlerEntry he) {
commandHandlers.put(command, he);
}
/**
* Builds the actual feature
*
* @return the feature which this template describes
*/
public DeviceFeature build() {
DeviceFeature f = new DeviceFeature(name);
f.setStatusFeature(isStatus);
f.setTimeout(timeout);
if (dispatcher != null) {
f.setMessageDispatcher(MessageDispatcher.makeHandler(dispatcher.getName(), dispatcher.getParams(), f));
}
if (pollHandler != null) {
f.setPollHandler(PollHandler.makeHandler(pollHandler, f));
}
if (defaultCmdHandler != null) {
f.setDefaultCommandHandler(
CommandHandler.makeHandler(defaultCmdHandler.getName(), defaultCmdHandler.getParams(), f));
}
if (defaultMsgHandler != null) {
f.setDefaultMsgHandler(
MessageHandler.makeHandler(defaultMsgHandler.getName(), defaultMsgHandler.getParams(), f));
}
for (Entry<Integer, HandlerEntry> mH : messageHandlers.entrySet()) {
f.addMessageHandler(mH.getKey(),
MessageHandler.makeHandler(mH.getValue().getName(), mH.getValue().getParams(), f));
}
for (Entry<Class<? extends Command>, HandlerEntry> cH : commandHandlers.entrySet()) {
f.addCommandHandler(cH.getKey(),
CommandHandler.makeHandler(cH.getValue().getName(), cH.getValue().getParams(), f));
}
return f;
}
@Override
public String toString() {
return getName() + "(" + isStatusFeature() + ")";
}
}

View File

@@ -0,0 +1,166 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.insteon.internal.device;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.insteon.internal.utils.Utils;
import org.openhab.binding.insteon.internal.utils.Utils.ParsingException;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.IncreaseDecreaseType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.PercentType;
import org.openhab.core.types.Command;
import org.w3c.dom.DOMException;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
/**
* Class that loads the device feature templates from an xml stream
*
* @author Daniel Pfrommer - Initial contribution
* @author Rob Nielsen - Port to openHAB 2 insteon binding
*/
@NonNullByDefault
public class FeatureTemplateLoader {
public static List<FeatureTemplate> readTemplates(InputStream input) throws IOException, ParsingException {
List<FeatureTemplate> features = new ArrayList<>();
try {
DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder dBuilder = dbFactory.newDocumentBuilder();
// Parse it!
Document doc = dBuilder.parse(input);
doc.getDocumentElement().normalize();
Element root = doc.getDocumentElement();
NodeList nodes = root.getChildNodes();
for (int i = 0; i < nodes.getLength(); i++) {
Node node = nodes.item(i);
if (node.getNodeType() == Node.ELEMENT_NODE) {
Element e = (Element) node;
if (e.getTagName().equals("feature")) {
features.add(parseFeature(e));
}
}
}
} catch (SAXException e) {
throw new ParsingException("Failed to parse XML!", e);
} catch (ParserConfigurationException e) {
throw new ParsingException("Got parser config exception! ", e);
}
return features;
}
private static FeatureTemplate parseFeature(Element e) throws ParsingException {
String name = e.getAttribute("name");
boolean statusFeature = e.getAttribute("statusFeature").equals("true");
FeatureTemplate feature = new FeatureTemplate(name, statusFeature, e.getAttribute("timeout"));
NodeList nodes = e.getChildNodes();
for (int i = 0; i < nodes.getLength(); i++) {
Node node = nodes.item(i);
if (node.getNodeType() == Node.ELEMENT_NODE) {
Element child = (Element) node;
if (child.getTagName().equals("message-handler")) {
parseMessageHandler(child, feature);
} else if (child.getTagName().equals("command-handler")) {
parseCommandHandler(child, feature);
} else if (child.getTagName().equals("message-dispatcher")) {
parseMessageDispatcher(child, feature);
} else if (child.getTagName().equals("poll-handler")) {
parsePollHandler(child, feature);
}
}
}
return feature;
}
private static HandlerEntry makeHandlerEntry(Element e) throws ParsingException {
String handler = e.getTextContent();
if (handler == null) {
throw new ParsingException("Could not find Handler for: " + e.getTextContent());
}
NamedNodeMap attributes = e.getAttributes();
Map<String, @Nullable String> params = new HashMap<>();
for (int i = 0; i < attributes.getLength(); i++) {
Node n = attributes.item(i);
params.put(n.getNodeName(), n.getNodeValue());
}
return new HandlerEntry(handler, params);
}
private static void parseMessageHandler(Element e, FeatureTemplate f) throws DOMException, ParsingException {
HandlerEntry he = makeHandlerEntry(e);
if (e.getAttribute("default").equals("true")) {
f.setDefaultMessageHandler(he);
} else {
String attr = e.getAttribute("cmd");
int command = (attr == null) ? 0 : Utils.from0xHexString(attr);
f.addMessageHandler(command, he);
}
}
private static void parseCommandHandler(Element e, FeatureTemplate f) throws ParsingException {
HandlerEntry he = makeHandlerEntry(e);
if (e.getAttribute("default").equals("true")) {
f.setDefaultCommandHandler(he);
} else {
Class<? extends Command> command = parseCommandClass(e.getAttribute("command"));
f.addCommandHandler(command, he);
}
}
private static void parseMessageDispatcher(Element e, FeatureTemplate f) throws DOMException, ParsingException {
HandlerEntry he = makeHandlerEntry(e);
f.setMessageDispatcher(he);
}
private static void parsePollHandler(Element e, FeatureTemplate f) throws ParsingException {
HandlerEntry he = makeHandlerEntry(e);
f.setPollHandler(he);
}
private static Class<? extends Command> parseCommandClass(String c) throws ParsingException {
if (c.equals("OnOffType")) {
return OnOffType.class;
} else if (c.equals("PercentType")) {
return PercentType.class;
} else if (c.equals("DecimalType")) {
return DecimalType.class;
} else if (c.equals("IncreaseDecreaseType")) {
return IncreaseDecreaseType.class;
} else {
throw new ParsingException("Unknown Command Type");
}
}
}

View File

@@ -0,0 +1,206 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.insteon.internal.device;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.insteon.internal.utils.Utils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Ideally, Insteon ALL LINK messages are received in this order, and
* only a single one of each:
*
* BCAST (a broadcast message from the device to all group members)
* CLEAN (a cleanup point-to-point message to ensure more reliable transmission)
* SUCCESS (a broadcast report of success or failure of cleanup, with cmd1=0x06)
*
* But often, the BCAST, CLEAN and SUCCESS messages are retransmitted multiple times,
* or (less frequently) messages are lost. The present state machine was developed
* to remove duplicates, yet make sure that a single lost message does not cause
* the binding to miss an update.
*
* @formatter:off
* "SUCCESS"
* EXPECT_BCAST
* ^ / ^ \
* SUCCESS / / \ \ [BCAST]
* / /['CLEAN'] 'SUCCESS'\ \
* / / \ \
* / V CLEAN \ V
* "CLEAN" EXPECT_SUCCESS <-------------- EXPECT_CLEAN "BCAST"
* -------------->
* ['BCAST']
* @formatter:on
*
* How to read this diagram:
*
* Regular, expected, non-duplicate messages do not have any quotes around them,
* and lead to the obvious state transitions.
*
* The actions in [square brackets] are transitions that cause a state
* update to be published when they occur.
*
* The presence of double quotes indicates a duplicate that does not lead
* to any state transitions, i.e. it is simply ignored.
*
* Single quotes indicate a message that is the result of a single dropped
* message, and leads to a state transition, in some cases even to a state
* update to be published.
*
* For instance at the top of the diagram, if a "SUCCESS" message is received
* when in state EXPECT_BCAST, it is considered a duplicate (it has "").
*
* When in state EXPECT_SUCCESS though, receiving a ['BCAST'] is most likely because
* the SUCCESS message was missed, and therefore it is considered the result
* of a single lost message (has '' around it). The state changes to EXPECT_CLEAN,
* and the message should lead to publishing of a state update (it has [] around it).
*
* @author Bernd Pfrommer - Initial contribution
* @author Rob Nielsen - Port to openHAB 2 insteon binding
*/
@NonNullByDefault
public class GroupMessageStateMachine {
private final Logger logger = LoggerFactory.getLogger(GroupMessageStateMachine.class);
/**
* The different kinds of Insteon ALL Link (Group) messages that can be received.
* Here is a typical sequence:
* BCAST:
* IN:Cmd:0x50|fromAddress:20.AC.99|toAddress:00.00.01|messageFlags:0xCB=ALL_LINK_BROADCAST:3:2|command1:0x13|
* command2:0x00|
* CLEAN:
* IN:Cmd:0x50|fromAddress:20.AC.99|toAddress:23.9B.65|messageFlags:0x41=ALL_LINK_CLEANUP:1:0|command1:0x13|command2
* :0x01|
* SUCCESS:
* IN:Cmd:0x50|fromAddress:20.AC.99|toAddress:13.03.01|messageFlags:0xCB=ALL_LINK_BROADCAST:3:2|command1:0x06|
* command2:0x00|
*/
enum GroupMessage {
BCAST,
CLEAN,
SUCCESS;
};
/**
* The state of the machine (i.e. what message we are expecting next).
* The usual state should be EXPECT_BCAST
*/
enum State {
EXPECT_BCAST,
EXPECT_CLEAN,
EXPECT_SUCCESS
};
private State state = State.EXPECT_BCAST;
private long lastUpdated = 0;
private boolean publish = false;
private byte lastCmd1 = 0;
/**
* Advance the state machine and determine if update is genuine (no duplicate)
*
* @param a the group message (action) that was received
* @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
* @return true if the group message is not a duplicate
*/
public boolean action(GroupMessage a, InsteonAddress address, int group, byte cmd1) {
publish = false;
long currentTime = System.currentTimeMillis();
switch (state) {
case EXPECT_BCAST:
switch (a) {
case BCAST:
publish = true;
break; // missed() move state machine and pub!
case CLEAN:
publish = true;
break; // missed(BCAST)
case SUCCESS:
publish = false;
break;
} // missed(BCAST,CLEAN) or dup SUCCESS
break;
case EXPECT_CLEAN:
switch (a) {
case BCAST:
if (lastCmd1 == cmd1) {
if (currentTime > lastUpdated + 30000) {
if (logger.isDebugEnabled()) {
logger.debug(
"{} group {} cmd1 {} is not a dup BCAST, received last message over 30000 ms ago",
address, group, Utils.getHexByte(cmd1));
}
publish = true;
} else {
publish = false;
}
} else {
if (logger.isDebugEnabled()) {
logger.debug("{} group {} cmd1 {} is not a dup BCAST, last cmd1 {}", address, group,
Utils.getHexByte(cmd1), Utils.getHexByte(lastCmd1));
}
publish = true;
}
break; // missed(CLEAN, SUCCESS) or dup BCAST
case CLEAN:
publish = false;
break; // missed() move state machine, no pub
case SUCCESS:
publish = false;
break;
} // missed(CLEAN)
break;
case EXPECT_SUCCESS:
switch (a) {
case BCAST:
publish = true;
break; // missed(SUCCESS)
case CLEAN:
publish = false;
break; // missed(SUCCESS,BCAST) or dup CLEAN
case SUCCESS:
publish = false;
break;
} // missed(), move state machine, no pub
break;
}
State oldState = state;
switch (a) {
case BCAST:
state = State.EXPECT_CLEAN;
break;
case CLEAN:
state = State.EXPECT_SUCCESS;
break;
case SUCCESS:
state = State.EXPECT_BCAST;
break;
}
lastCmd1 = cmd1;
lastUpdated = currentTime;
logger.debug("{} group {} state: {} --{}--> {}, publish: {}", address, group, oldState, a, state, publish);
return (publish);
}
public long getLastUpdated() {
return lastUpdated;
}
public boolean getPublish() {
return publish;
}
}

View File

@@ -0,0 +1,43 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.insteon.internal.device;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* Ugly little helper class to facilitate late instantiation of handlers
*
* @author Bernd Pfrommer - Initial contribution
* @author Rob Nielsen - Port to openHAB 2 insteon binding
*/
@NonNullByDefault
public class HandlerEntry {
Map<String, @Nullable String> params;
String name;
HandlerEntry(String name, Map<String, @Nullable String> params) {
this.name = name;
this.params = params;
}
Map<String, @Nullable String> getParams() {
return params;
}
String getName() {
return name;
}
}

View File

@@ -0,0 +1,225 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.insteon.internal.device;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.insteon.internal.utils.Utils;
/**
* This class wraps an Insteon Address 'xx.xx.xx'
*
* @author Daniel Pfrommer - Initial contribution
* @author Rob Nielsen - Port to openHAB 2 insteon binding
*/
@NonNullByDefault
public class InsteonAddress {
private byte highByte;
private byte middleByte;
private byte lowByte;
private boolean x10;
public InsteonAddress() {
highByte = 0x00;
middleByte = 0x00;
lowByte = 0x00;
x10 = false;
}
public InsteonAddress(InsteonAddress a) {
highByte = a.highByte;
middleByte = a.middleByte;
lowByte = a.lowByte;
x10 = a.x10;
}
public InsteonAddress(byte high, byte middle, byte low) {
highByte = high;
middleByte = middle;
lowByte = low;
x10 = false;
}
/**
* Constructor
*
* @param address string must have format of e.g. '2a.3c.40' or (for X10) 'H.UU'
*/
public InsteonAddress(String address) throws IllegalArgumentException {
if (X10.isValidAddress(address)) {
highByte = 0;
middleByte = 0;
lowByte = X10.addressToByte(address);
x10 = true;
} else {
String[] parts = address.split("\\.");
if (parts.length != 3) {
throw new IllegalArgumentException("Address string must have 3 bytes, has: " + parts.length);
}
highByte = (byte) Utils.fromHexString(parts[0]);
middleByte = (byte) Utils.fromHexString(parts[1]);
lowByte = (byte) Utils.fromHexString(parts[2]);
x10 = false;
}
}
/**
* Constructor for an InsteonAddress that wraps an X10 address.
* Simply stuff the X10 address into the lowest byte.
*
* @param aX10HouseUnit the house & unit number as encoded by the X10 protocol
*/
public InsteonAddress(byte aX10HouseUnit) {
highByte = 0;
middleByte = 0;
lowByte = aX10HouseUnit;
x10 = true;
}
public void setHighByte(byte h) {
highByte = h;
}
public void setMiddleByte(byte m) {
middleByte = m;
}
public void setLowByte(byte l) {
lowByte = l;
}
public byte getHighByte() {
return highByte;
}
public byte getMiddleByte() {
return middleByte;
}
public byte getLowByte() {
return lowByte;
}
public byte getX10HouseCode() {
return (byte) ((lowByte & 0xf0) >> 4);
}
public byte getX10UnitCode() {
return (byte) ((lowByte & 0x0f));
}
public boolean isX10() {
return x10;
}
public void storeBytes(byte[] bytes, int offset) {
bytes[offset] = getHighByte();
bytes[offset + 1] = getMiddleByte();
bytes[offset + 2] = getLowByte();
}
public void loadBytes(byte[] bytes, int offset) {
setHighByte(bytes[offset]);
setMiddleByte(bytes[offset + 1]);
setLowByte(bytes[offset + 2]);
}
@Override
public String toString() {
String s = null;
if (isX10()) {
byte house = (byte) (((getLowByte() & 0xf0) >> 4) & 0xff);
byte unit = (byte) ((getLowByte() & 0x0f) & 0xff);
s = X10.houseToString(house) + "." + X10.unitToInt(unit);
// s = Utils.getHexString(lowByte);
} else {
s = Utils.getHexString(highByte) + "." + Utils.getHexString(middleByte) + "." + Utils.getHexString(lowByte);
}
return s;
}
@Override
public boolean equals(@Nullable Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
InsteonAddress other = (InsteonAddress) obj;
if (highByte != other.highByte) {
return false;
}
if (lowByte != other.lowByte) {
return false;
}
if (middleByte != other.middleByte) {
return false;
}
if (x10 != other.x10) {
return false;
}
return true;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + highByte;
result = prime * result + lowByte;
result = prime * result + middleByte;
result = prime * result + (x10 ? 1231 : 1237);
return result;
}
/**
* Test if Insteon address is valid
*
* @return true if address is in valid AB.CD.EF or (for X10) H.UU format
*/
public static boolean isValid(@Nullable String addr) {
if (addr == null) {
return false;
}
if (X10.isValidAddress(addr)) {
return true;
}
String[] fields = addr.split("\\.");
if (fields.length != 3) {
return false;
}
try {
// convert the insteon xx.xx.xx address to integer to test
@SuppressWarnings("unused")
int test = Integer.parseInt(fields[2], 16) * 65536 + Integer.parseInt(fields[1], 16) * 256
+ +Integer.parseInt(fields[0], 16);
} catch (NumberFormatException e) {
return false;
}
return true;
}
/**
* Turn string into address
*
* @param val the string to convert
* @return the corresponding insteon address
*/
public static InsteonAddress parseAddress(String val) {
return new InsteonAddress(val);
}
}

View File

@@ -0,0 +1,639 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.insteon.internal.device;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.PriorityQueue;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.insteon.internal.config.InsteonChannelConfiguration;
import org.openhab.binding.insteon.internal.device.DeviceType.FeatureGroup;
import org.openhab.binding.insteon.internal.device.GroupMessageStateMachine.GroupMessage;
import org.openhab.binding.insteon.internal.driver.Driver;
import org.openhab.binding.insteon.internal.message.FieldException;
import org.openhab.binding.insteon.internal.message.InvalidMessageTypeException;
import org.openhab.binding.insteon.internal.message.Msg;
import org.openhab.core.types.Command;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The InsteonDevice class holds known per-device state of a single Insteon device,
* including the address, what port(modem) to reach it on etc.
* Note that some Insteon devices de facto consist of two devices (let's say
* a relay and a sensor), but operate under the same address. Such devices will
* be represented just by a single InsteonDevice. Their different personalities
* will then be represented by DeviceFeatures.
*
* @author Bernd Pfrommer - Initial contribution
* @author Rob Nielsen - Port to openHAB 2 insteon binding
*/
@NonNullByDefault
@SuppressWarnings("null")
public class InsteonDevice {
private final Logger logger = LoggerFactory.getLogger(InsteonDevice.class);
public static enum DeviceStatus {
INITIALIZED,
POLLING
}
/** need to wait after query to avoid misinterpretation of duplicate replies */
private static final int QUIET_TIME_DIRECT_MESSAGE = 2000;
/** how far to space out poll messages */
private static final int TIME_BETWEEN_POLL_MESSAGES = 1500;
private InsteonAddress address = new InsteonAddress();
private long pollInterval = -1L; // in milliseconds
private @Nullable Driver driver = null;
private HashMap<String, @Nullable DeviceFeature> features = new HashMap<>();
private @Nullable String productKey = null;
private volatile long lastTimePolled = 0L;
private volatile long lastMsgReceived = 0L;
private boolean isModem = false;
private PriorityQueue<@Nullable QEntry> mrequestQueue = new PriorityQueue<>();
private @Nullable DeviceFeature featureQueried = null;
private long lastQueryTime = 0L;
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
*/
public InsteonDevice() {
lastMsgReceived = System.currentTimeMillis();
}
// --------------------- simple getters -----------------------------
public boolean hasProductKey() {
return productKey != null;
}
public @Nullable String getProductKey() {
return productKey;
}
public boolean hasModemDBEntry() {
return hasModemDBEntry;
}
public DeviceStatus getStatus() {
return status;
}
public InsteonAddress getAddress() {
return (address);
}
public @Nullable Driver getDriver() {
return driver;
}
public long getPollInterval() {
return pollInterval;
}
public boolean isModem() {
return isModem;
}
public @Nullable DeviceFeature getFeature(String f) {
return features.get(f);
}
public HashMap<String, @Nullable DeviceFeature> getFeatures() {
return features;
}
public byte getX10HouseCode() {
return (address.getX10HouseCode());
}
public byte getX10UnitCode() {
return (address.getX10UnitCode());
}
public boolean hasProductKey(String key) {
return productKey != null && productKey.equals(key);
}
public boolean hasValidPollingInterval() {
return (pollInterval > 0);
}
public long getPollOverDueTime() {
return (lastTimePolled - lastMsgReceived);
}
public boolean hasAnyListeners() {
synchronized (features) {
for (DeviceFeature f : features.values()) {
if (f.hasListeners()) {
return true;
}
}
}
return false;
}
// --------------------- simple setters -----------------------------
public void setStatus(DeviceStatus aI) {
status = aI;
}
public void setHasModemDBEntry(boolean b) {
hasModemDBEntry = b;
}
public void setAddress(InsteonAddress ia) {
address = ia;
}
public void setDriver(Driver d) {
driver = d;
}
public void setIsModem(boolean f) {
isModem = f;
}
public void setProductKey(String pk) {
productKey = pk;
}
public void setPollInterval(long pi) {
logger.trace("setting poll interval for {} to {} ", address, pi);
if (pi > 0) {
pollInterval = pi;
}
}
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) {
return (featureQueried);
}
}
/**
* Removes feature listener from this device
*
* @param aItemName name of the feature listener to remove
* @return true if a feature listener was successfully removed
*/
public boolean removeFeatureListener(String aItemName) {
boolean removedListener = false;
synchronized (features) {
for (Iterator<Entry<String, @Nullable DeviceFeature>> it = features.entrySet().iterator(); it.hasNext();) {
DeviceFeature f = it.next().getValue();
if (f.removeListener(aItemName)) {
removedListener = true;
}
}
}
return removedListener;
}
/**
* Invoked to process an openHAB command
*
* @param driver The driver to use
* @param c The item configuration
* @param command The actual command to execute
*/
public void processCommand(Driver driver, InsteonChannelConfiguration c, Command command) {
logger.debug("processing command {} features: {}", command, features.size());
synchronized (features) {
for (DeviceFeature i : features.values()) {
if (i.isReferencedByItem(c.getChannelName())) {
i.handleCommand(c, command);
}
}
}
}
/**
* Execute poll on this device: create an array of messages,
* add them to the request queue, and schedule the queue
* for processing.
*
* @param delay scheduling delay (in milliseconds)
*/
public void doPoll(long delay) {
long now = System.currentTimeMillis();
List<QEntry> l = new ArrayList<>();
synchronized (features) {
int spacing = 0;
for (DeviceFeature i : features.values()) {
if (i.hasListeners()) {
Msg m = i.makePollMsg();
if (m != null) {
l.add(new QEntry(i, m, now + delay + spacing));
spacing += TIME_BETWEEN_POLL_MESSAGES;
}
}
}
}
if (l.isEmpty()) {
return;
}
synchronized (mrequestQueue) {
for (QEntry e : l) {
mrequestQueue.add(e);
}
}
RequestQueueManager.instance().addQueue(this, now + delay);
if (!l.isEmpty()) {
lastTimePolled = now;
}
}
/**
* Handle incoming message for this device by forwarding
* it to all features that this device supports
*
* @param msg the incoming message
*/
public void handleMessage(Msg msg) {
lastMsgReceived = System.currentTimeMillis();
synchronized (features) {
// first update all features that are
// not status features
for (DeviceFeature f : features.values()) {
if (!f.isStatusFeature()) {
logger.debug("----- applying message to feature: {}", f.getName());
if (f.handleMessage(msg)) {
// handled a reply to a query,
// mark it as processed
logger.trace("handled reply of direct: {}", f);
setFeatureQueried(null);
break;
}
}
}
// then update all the status features,
// e.g. when the device was last updated
for (DeviceFeature f : features.values()) {
if (f.isStatusFeature()) {
f.handleMessage(msg);
}
}
}
}
/**
* Helper method to make standard message
*
* @param flags
* @param cmd1
* @param cmd2
* @return standard message
* @throws FieldException
* @throws IOException
*/
public Msg makeStandardMessage(byte flags, byte cmd1, byte cmd2)
throws FieldException, InvalidMessageTypeException {
return (makeStandardMessage(flags, cmd1, cmd2, -1));
}
/**
* Helper method to make standard message, possibly with group
*
* @param flags
* @param cmd1
* @param cmd2
* @param group (-1 if not a group message)
* @return standard message
* @throws FieldException
* @throws IOException
*/
public Msg makeStandardMessage(byte flags, byte cmd1, byte cmd2, int group)
throws FieldException, InvalidMessageTypeException {
Msg m = Msg.makeMessage("SendStandardMessage");
InsteonAddress addr = null;
byte f = flags;
if (group != -1) {
f |= 0xc0; // mark message as group message
// and stash the group number into the address
addr = new InsteonAddress((byte) 0, (byte) 0, (byte) (group & 0xff));
} else {
addr = getAddress();
}
m.setAddress("toAddress", addr);
m.setByte("messageFlags", f);
m.setByte("command1", cmd1);
m.setByte("command2", cmd2);
return m;
}
public Msg makeX10Message(byte rawX10, byte X10Flag) throws FieldException, InvalidMessageTypeException {
Msg m = Msg.makeMessage("SendX10Message");
m.setByte("rawX10", rawX10);
m.setByte("X10Flag", X10Flag);
m.setQuietTime(300L);
return m;
}
/**
* Helper method to make extended message
*
* @param flags
* @param cmd1
* @param cmd2
* @return extended message
* @throws FieldException
* @throws IOException
*/
public Msg makeExtendedMessage(byte flags, byte cmd1, byte cmd2)
throws FieldException, InvalidMessageTypeException {
return makeExtendedMessage(flags, cmd1, cmd2, new byte[] {});
}
/**
* Helper method to make extended message
*
* @param flags
* @param cmd1
* @param cmd2
* @param data array with userdata
* @return extended message
* @throws FieldException
* @throws IOException
*/
public Msg makeExtendedMessage(byte flags, byte cmd1, byte cmd2, byte[] data)
throws FieldException, InvalidMessageTypeException {
Msg m = Msg.makeMessage("SendExtendedMessage");
m.setAddress("toAddress", getAddress());
m.setByte("messageFlags", (byte) (((flags & 0xff) | 0x10) & 0xff));
m.setByte("command1", cmd1);
m.setByte("command2", cmd2);
m.setUserData(data);
m.setCRC();
return m;
}
/**
* Helper method to make extended message, but with different CRC calculation
*
* @param flags
* @param cmd1
* @param cmd2
* @param data array with user data
* @return extended message
* @throws FieldException
* @throws IOException
*/
public Msg makeExtendedMessageCRC2(byte flags, byte cmd1, byte cmd2, byte[] data)
throws FieldException, InvalidMessageTypeException {
Msg m = Msg.makeMessage("SendExtendedMessage");
m.setAddress("toAddress", getAddress());
m.setByte("messageFlags", (byte) (((flags & 0xff) | 0x10) & 0xff));
m.setByte("command1", cmd1);
m.setByte("command2", cmd2);
m.setUserData(data);
m.setCRC2();
return m;
}
/**
* Called by the RequestQueueManager when the queue has expired
*
* @param timeNow
* @return time when to schedule the next message (timeNow + quietTime)
*/
public long processRequestQueue(long timeNow) {
synchronized (mrequestQueue) {
if (mrequestQueue.isEmpty()) {
return 0L;
}
if (featureQueried != null) {
// A feature has been queried, but
// the response has not been digested yet.
// Must wait for the query to be processed.
long dt = timeNow - (lastQueryTime + featureQueried.getDirectAckTimeout());
if (dt < 0) {
logger.debug("still waiting for query reply from {} for another {} usec", address, -dt);
return (timeNow + 2000L); // retry soon
} else {
logger.debug("gave up waiting for query reply from device {}", address);
}
}
QEntry qe = mrequestQueue.poll(); // take it off the queue!
if (!qe.getMsg().isBroadcast()) {
logger.debug("qe taken off direct: {} {}", qe.getFeature(), qe.getMsg());
lastQueryTime = timeNow;
// mark feature as pending
qe.getFeature().setQueryStatus(DeviceFeature.QueryStatus.QUERY_PENDING);
// also mark this queue as pending so there is no doubt
featureQueried = qe.getFeature();
} else {
logger.debug("qe taken off bcast: {} {}", qe.getFeature(), qe.getMsg());
}
long quietTime = qe.getMsg().getQuietTime();
qe.getMsg().setQuietTime(500L); // rate limiting downstream!
try {
writeMessage(qe.getMsg());
} catch (IOException e) {
logger.warn("message write failed for msg {}", qe.getMsg(), e);
}
// figure out when the request queue should be checked next
QEntry qnext = mrequestQueue.peek();
long nextExpTime = (qnext == null ? 0L : qnext.getExpirationTime());
long nextTime = Math.max(timeNow + quietTime, nextExpTime);
logger.debug("next request queue processed in {} msec, quiettime = {}", nextTime - timeNow, quietTime);
return (nextTime);
}
}
/**
* Enqueues message to be sent at the next possible time
*
* @param m message to be sent
* @param f device feature that sent this message (so we can associate the response message with it)
*/
public void enqueueMessage(Msg m, DeviceFeature f) {
enqueueDelayedMessage(m, f, 0);
}
/**
* Enqueues message to be sent after a delay
*
* @param m message to be sent
* @param f device feature that sent this message (so we can associate the response message with it)
* @param d time (in milliseconds)to delay before enqueuing message
*/
public void enqueueDelayedMessage(Msg m, DeviceFeature f, long delay) {
long now = System.currentTimeMillis();
synchronized (mrequestQueue) {
mrequestQueue.add(new QEntry(f, m, now + delay));
}
if (!m.isBroadcast()) {
m.setQuietTime(QUIET_TIME_DIRECT_MESSAGE);
}
logger.trace("enqueing direct message with delay {}", delay);
RequestQueueManager.instance().addQueue(this, now + delay);
}
private void writeMessage(Msg m) throws IOException {
driver.writeMessage(m);
}
private void instantiateFeatures(@Nullable DeviceType dt) {
for (Entry<String, String> fe : dt.getFeatures().entrySet()) {
DeviceFeature f = DeviceFeature.makeDeviceFeature(fe.getValue());
if (f == null) {
logger.warn("device type {} references unknown feature: {}", dt, fe.getValue());
} else {
addFeature(fe.getKey(), f);
}
}
for (Entry<String, FeatureGroup> fe : dt.getFeatureGroups().entrySet()) {
FeatureGroup fg = fe.getValue();
@Nullable
DeviceFeature f = DeviceFeature.makeDeviceFeature(fg.getType());
if (f == null) {
logger.warn("device type {} references unknown feature group: {}", dt, fg.getType());
} else {
addFeature(fe.getKey(), f);
connectFeatures(fe.getKey(), f, fg.getFeatures());
}
}
}
private void connectFeatures(String gn, DeviceFeature fg, ArrayList<String> fgFeatures) {
for (String fs : fgFeatures) {
@Nullable
DeviceFeature f = features.get(fs);
if (f == null) {
logger.warn("feature group {} references unknown feature {}", gn, fs);
} else {
logger.debug("{} connected feature: {}", gn, f);
fg.addConnectedFeature(f);
}
}
}
private void addFeature(String name, DeviceFeature f) {
f.setDevice(this);
synchronized (features) {
features.put(name, f);
}
}
/**
* Get the state of the state machine that suppresses duplicates for group messages.
* The state machine is advance the first time it is called for a message,
* otherwise return the current state.
*
* @param group the insteon group of the broadcast message
* @param a the type of group message came in (action etc)
* @param cmd1 cmd1 from the message received
* @return true if this is message is NOT a duplicate
*/
public boolean getGroupState(int group, GroupMessage a, byte cmd1) {
GroupMessageStateMachine m = groupState.get(group);
if (m == null) {
m = new GroupMessageStateMachine();
groupState.put(group, m);
logger.trace("{} created group {} state", address, group);
} else {
if (lastMsgReceived <= m.getLastUpdated()) {
logger.trace("{} using previous group {} state for {}", address, group, a);
return m.getPublish();
}
}
logger.trace("{} updating group {} state to {}", address, group, a);
return (m.action(a, address, group, cmd1));
}
@Override
public String toString() {
String s = address.toString();
for (Entry<String, @Nullable DeviceFeature> f : features.entrySet()) {
s += "|" + f.getKey() + "->" + f.getValue().toString();
}
return s;
}
/**
* Factory method
*
* @param dt device type after which to model the device
* @return newly created device
*/
public static InsteonDevice makeDevice(@Nullable DeviceType dt) {
InsteonDevice dev = new InsteonDevice();
dev.instantiateFeatures(dt);
return dev;
}
/**
* Queue entry helper class
*
* @author Bernd Pfrommer - Initial contribution
*/
@NonNullByDefault
public static class QEntry implements Comparable<QEntry> {
private DeviceFeature feature;
private Msg msg;
private long expirationTime;
public DeviceFeature getFeature() {
return feature;
}
public Msg getMsg() {
return msg;
}
public long getExpirationTime() {
return expirationTime;
}
QEntry(DeviceFeature f, Msg m, long t) {
feature = f;
msg = m;
expirationTime = t;
}
@Override
public int compareTo(QEntry a) {
return (int) (expirationTime - a.expirationTime);
}
}
}

View File

@@ -0,0 +1,416 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.insteon.internal.device;
import java.lang.reflect.InvocationTargetException;
import java.util.HashMap;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.insteon.internal.message.FieldException;
import org.openhab.binding.insteon.internal.message.Msg;
import org.openhab.binding.insteon.internal.utils.Utils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Does preprocessing of messages to decide which handler should be called.
*
* @author Bernd Pfrommer - Initial contribution
* @author Rob Nielsen - Port to openHAB 2 insteon binding
*/
@NonNullByDefault
@SuppressWarnings("null")
public abstract class MessageDispatcher {
private static final Logger logger = LoggerFactory.getLogger(MessageDispatcher.class);
DeviceFeature feature;
@Nullable
Map<String, @Nullable String> parameters = new HashMap<>();
/**
* Constructor
*
* @param f DeviceFeature to which this MessageDispatcher belongs
*/
MessageDispatcher(DeviceFeature f) {
feature = f;
}
public void setParameters(@Nullable Map<String, @Nullable String> map) {
parameters = map;
}
/**
* Generic handling of incoming ALL LINK messages
*
* @param msg the message received
* @return true if the message was handled by this function
*/
protected boolean handleAllLinkMessage(Msg msg) {
if (!msg.isAllLink()) {
return false;
}
try {
InsteonAddress a = msg.getAddress("toAddress");
// ALL_LINK_BROADCAST and ALL_LINK_CLEANUP
// have a valid Command1 field
// but the CLEANUP_SUCCESS (of type ALL_LINK_BROADCAST!)
// message has cmd1 = 0x06 and the cmd as the
// high byte of the toAddress.
byte cmd1 = msg.getByte("command1");
if (!msg.isCleanup() && cmd1 == 0x06) {
cmd1 = a.getHighByte();
}
// For ALL_LINK_BROADCAST messages, the group is
// in the low byte of the toAddress. For direct
// ALL_LINK_CLEANUP, it is in Command2
int group = (msg.isCleanup() ? msg.getByte("command2") : a.getLowByte()) & 0xff;
MessageHandler h = feature.getMsgHandlers().get(cmd1 & 0xFF);
if (h == null) {
logger.debug("msg is not for this feature");
return true;
}
if (!h.isDuplicate(msg)) {
if (h.matchesGroup(group) && h.matches(msg)) {
logger.debug("{}:{}->{} cmd1:{} group {}/{}", feature.getDevice().getAddress(), feature.getName(),
h.getClass().getSimpleName(), Utils.getHexByte(cmd1), group, h.getGroup());
h.handleMessage(group, cmd1, msg, feature);
} else {
logger.debug("message ignored because matches group: {} matches filter: {}", h.matchesGroup(group),
h.matches(msg));
}
} else {
logger.debug("message ignored as duplicate. Matches group: {} matches filter: {}",
h.matchesGroup(group), h.matches(msg));
}
} catch (FieldException e) {
logger.warn("couldn't parse ALL_LINK message: {}", msg, e);
}
return true;
}
/**
* Checks if this message is in response to previous query by this feature
*
* @param msg
* @return true;
*/
boolean isMyDirectAck(Msg msg) {
return msg.isAckOfDirect() && (feature.getQueryStatus() == DeviceFeature.QueryStatus.QUERY_PENDING)
&& feature.getDevice().getFeatureQueried() == feature;
}
/**
* Dispatches message
*
* @param msg Message to dispatch
* @return true if this message was found to be a reply to a direct message,
* and was claimed by one of the handlers
*/
public abstract boolean dispatch(Msg msg);
//
//
// ------------ implementations of MessageDispatchers start here ------------------
//
//
@NonNullByDefault
public static class DefaultDispatcher extends MessageDispatcher {
DefaultDispatcher(DeviceFeature f) {
super(f);
}
@Override
public boolean dispatch(Msg msg) {
byte cmd = 0x00;
byte cmd1 = 0x00;
boolean isConsumed = false;
int key = -1;
try {
cmd = msg.getByte("Cmd");
cmd1 = msg.getByte("command1");
} catch (FieldException e) {
logger.debug("no command found, dropping msg {}", msg);
return false;
}
if (msg.isAllLinkCleanupAckOrNack()) {
// Had cases when a KeypadLinc would send an ALL_LINK_CLEANUP_ACK
// in response to a direct status query message
return false;
}
if (handleAllLinkMessage(msg)) {
return false;
}
if (msg.isAckOfDirect()) {
// in the case of direct ack, the cmd1 code is useless.
// you have to know what message was sent before to
// interpret the reply message
if (isMyDirectAck(msg)) {
logger.debug("{}:{} DIRECT_ACK: q:{} cmd: {}", feature.getDevice().getAddress(), feature.getName(),
feature.getQueryStatus(), cmd);
isConsumed = true;
if (cmd == 0x50) {
// must be a reply to our message, tweak the cmd1 code!
logger.debug("changing key to 0x19 for msg {}", msg);
key = 0x19; // we have installed a handler under that command number
}
}
} else {
key = (cmd1 & 0xFF);
}
if (key != -1 || feature.isStatusFeature()) {
MessageHandler h = feature.getMsgHandlers().get(key);
if (h == null) {
h = feature.getDefaultMsgHandler();
}
if (h.matches(msg)) {
if (!isConsumed) {
logger.debug("{}:{}->{} DIRECT", feature.getDevice().getAddress(), feature.getName(),
h.getClass().getSimpleName());
}
h.handleMessage(-1, cmd1, msg, feature);
}
}
if (isConsumed) {
feature.setQueryStatus(DeviceFeature.QueryStatus.QUERY_ANSWERED);
logger.debug("defdisp: {}:{} set status to: {}", feature.getDevice().getAddress(), feature.getName(),
feature.getQueryStatus());
}
return isConsumed;
}
}
@NonNullByDefault
public static class DefaultGroupDispatcher extends MessageDispatcher {
DefaultGroupDispatcher(DeviceFeature f) {
super(f);
}
@Override
public boolean dispatch(Msg msg) {
byte cmd = 0x00;
byte cmd1 = 0x00;
boolean isConsumed = false;
int key = -1;
try {
cmd = msg.getByte("Cmd");
cmd1 = msg.getByte("command1");
} catch (FieldException e) {
logger.debug("no command found, dropping msg {}", msg);
return false;
}
if (msg.isAllLinkCleanupAckOrNack()) {
// Had cases when a KeypadLinc would send an ALL_LINK_CLEANUP_ACK
// in response to a direct status query message
return false;
}
if (handleAllLinkMessage(msg)) {
return false;
}
if (msg.isAckOfDirect()) {
// in the case of direct ack, the cmd1 code is useless.
// you have to know what message was sent before to
// interpret the reply message
if (isMyDirectAck(msg)) {
logger.debug("{}:{} qs:{} cmd: {}", feature.getDevice().getAddress(), feature.getName(),
feature.getQueryStatus(), cmd);
isConsumed = true;
if (cmd == 0x50) {
// must be a reply to our message, tweak the cmd1 code!
logger.debug("changing key to 0x19 for msg {}", msg);
key = 0x19; // we have installed a handler under that command number
}
}
} else {
key = (cmd1 & 0xFF);
}
if (key != -1) {
for (DeviceFeature f : feature.getConnectedFeatures()) {
MessageHandler h = f.getMsgHandlers().get(key);
if (h == null) {
h = f.getDefaultMsgHandler();
}
if (h.matches(msg)) {
if (!isConsumed) {
logger.debug("{}:{}->{} DIRECT", f.getDevice().getAddress(), f.getName(),
h.getClass().getSimpleName());
}
h.handleMessage(-1, cmd1, msg, f);
}
}
}
if (isConsumed) {
feature.setQueryStatus(DeviceFeature.QueryStatus.QUERY_ANSWERED);
logger.debug("{}:{} set status to: {}", feature.getDevice().getAddress(), feature.getName(),
feature.getQueryStatus());
}
return isConsumed;
}
}
@NonNullByDefault
public static class PollGroupDispatcher extends MessageDispatcher {
PollGroupDispatcher(DeviceFeature f) {
super(f);
}
@Override
public boolean dispatch(Msg msg) {
if (msg.isAllLinkCleanupAckOrNack()) {
// Had cases when a KeypadLinc would send an ALL_LINK_CLEANUP_ACK
// in response to a direct status query message
return false;
}
if (handleAllLinkMessage(msg)) {
return false;
}
if (msg.isAckOfDirect()) {
boolean isMyAck = isMyDirectAck(msg);
if (isMyAck) {
logger.debug("{}:{} got poll ACK", feature.getDevice().getAddress(), feature.getName());
}
return (isMyAck);
}
return (false); // not a direct ack, so we didn't consume it either
}
}
@NonNullByDefault
public static class SimpleDispatcher extends MessageDispatcher {
SimpleDispatcher(DeviceFeature f) {
super(f);
}
@Override
public boolean dispatch(Msg msg) {
byte cmd1 = 0x00;
try {
if (handleAllLinkMessage(msg)) {
return false;
}
if (msg.isAllLinkCleanupAckOrNack()) {
// Had cases when a KeypadLinc would send an ALL_LINK_CLEANUP_ACK
// in response to a direct status query message
return false;
}
cmd1 = msg.getByte("command1");
} catch (FieldException e) {
logger.debug("no cmd1 found, dropping msg {}", msg);
return false;
}
boolean isConsumed = isMyDirectAck(msg);
int key = (cmd1 & 0xFF);
MessageHandler h = feature.getMsgHandlers().get(key);
if (h == null) {
h = feature.getDefaultMsgHandler();
}
if (h.matches(msg)) {
logger.trace("{}:{}->{} {}", feature.getDevice().getAddress(), feature.getName(),
h.getClass().getSimpleName(), msg);
h.handleMessage(-1, cmd1, msg, feature);
}
return isConsumed;
}
}
@NonNullByDefault
public static class X10Dispatcher extends MessageDispatcher {
X10Dispatcher(DeviceFeature f) {
super(f);
}
@Override
public boolean dispatch(Msg msg) {
try {
byte rawX10 = msg.getByte("rawX10");
int cmd = (rawX10 & 0x0f);
MessageHandler h = feature.getMsgHandlers().get(cmd);
if (h == null) {
h = feature.getDefaultMsgHandler();
}
logger.debug("{}:{}->{} {}", feature.getDevice().getAddress(), feature.getName(),
h.getClass().getSimpleName(), msg);
if (h.matches(msg)) {
h.handleMessage(-1, (byte) cmd, msg, feature);
}
} catch (FieldException e) {
logger.warn("error parsing {}: ", msg, e);
}
return false;
}
}
@NonNullByDefault
public static class PassThroughDispatcher extends MessageDispatcher {
PassThroughDispatcher(DeviceFeature f) {
super(f);
}
@Override
public boolean dispatch(Msg msg) {
MessageHandler h = feature.getDefaultMsgHandler();
if (h.matches(msg)) {
logger.trace("{}:{}->{} {}", feature.getDevice().getAddress(), feature.getName(),
h.getClass().getSimpleName(), msg);
h.handleMessage(-1, (byte) 0x01, msg, feature);
}
return false;
}
}
/**
* Drop all incoming messages silently
*/
@NonNullByDefault
public static class NoOpDispatcher extends MessageDispatcher {
NoOpDispatcher(DeviceFeature f) {
super(f);
}
@Override
public boolean dispatch(Msg msg) {
return false;
}
}
/**
* Factory method for creating a dispatcher of a given name using java reflection
*
* @param name the name of the dispatcher to create
* @param params
* @param f the feature for which to create the dispatcher
* @return the handler which was created
*/
@Nullable
public static <T extends MessageDispatcher> T makeHandler(String name,
@Nullable Map<String, @Nullable String> params, DeviceFeature f) {
String cname = MessageDispatcher.class.getName() + "$" + name;
try {
Class<?> c = Class.forName(cname);
@SuppressWarnings("unchecked")
Class<? extends T> dc = (Class<? extends T>) c;
T ch = dc.getDeclaredConstructor(DeviceFeature.class).newInstance(f);
ch.setParameters(params);
return ch;
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException | IllegalArgumentException
| InvocationTargetException | NoSuchMethodException | SecurityException e) {
logger.warn("error trying to create dispatcher: {}", name, e);
}
return null;
}
}

View File

@@ -0,0 +1,205 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.insteon.internal.device;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.insteon.internal.driver.ModemDBEntry;
import org.openhab.binding.insteon.internal.driver.Port;
import org.openhab.binding.insteon.internal.message.FieldException;
import org.openhab.binding.insteon.internal.message.InvalidMessageTypeException;
import org.openhab.binding.insteon.internal.message.Msg;
import org.openhab.binding.insteon.internal.message.MsgListener;
import org.openhab.binding.insteon.internal.utils.Utils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Builds the modem database from incoming link record messages
*
* @author Bernd Pfrommer - Initial contribution
* @author Rob Nielsen - Port to openHAB 2 insteon binding
*/
@NonNullByDefault
@SuppressWarnings("null")
public class ModemDBBuilder implements MsgListener {
private static final int MESSAGE_TIMEOUT = 30000;
private final Logger logger = LoggerFactory.getLogger(ModemDBBuilder.class);
private volatile boolean isComplete = false;
private Port port;
private ScheduledExecutorService scheduler;
private @Nullable ScheduledFuture<?> job = null;
private volatile long lastMessageTimestamp;
private volatile int messageCount = 0;
public ModemDBBuilder(Port port, ScheduledExecutorService scheduler) {
this.port = port;
this.scheduler = scheduler;
}
public void start() {
port.addListener(this);
logger.trace("starting modem db builder");
startDownload();
job = scheduler.scheduleWithFixedDelay(() -> {
if (isComplete()) {
logger.trace("modem db builder finished");
job.cancel(false);
job = null;
} else {
if (System.currentTimeMillis() - lastMessageTimestamp > MESSAGE_TIMEOUT) {
String s = "";
if (messageCount == 0) {
s = " No messages were received, the PLM or hub might be broken. If this continues see "
+ "'Known Limitations and Issues' in the Insteon binding documentation.";
}
logger.warn("Modem database download was unsuccessful, restarting!{}", s);
startDownload();
}
}
}, 0, 1, TimeUnit.SECONDS);
}
private void startDownload() {
logger.trace("starting modem database download");
port.clearModemDB();
lastMessageTimestamp = System.currentTimeMillis();
messageCount = 0;
getFirstLinkRecord();
}
public boolean isComplete() {
return isComplete;
}
private void getFirstLinkRecord() {
try {
port.writeMessage(Msg.makeMessage("GetFirstALLLinkRecord"));
} catch (IOException e) {
logger.warn("error sending link record query ", e);
} catch (InvalidMessageTypeException e) {
logger.warn("invalid message ", e);
}
}
/**
* processes link record messages from the modem to build database
* and request more link records if not finished.
* {@inheritDoc}
*/
@Override
public void msg(Msg msg) {
lastMessageTimestamp = System.currentTimeMillis();
messageCount++;
if (msg.isPureNack()) {
return;
}
try {
if (msg.getByte("Cmd") == 0x69 || msg.getByte("Cmd") == 0x6a) {
// If the flag is "ACK/NACK", a record response
// will follow, so we do nothing here.
// If its "NACK", there are none
if (msg.getByte("ACK/NACK") == 0x15) {
logger.debug("got all link records.");
done();
}
} else if (msg.getByte("Cmd") == 0x57) {
// we got the link record response
updateModemDB(msg.getAddress("LinkAddr"), port, msg, false);
port.writeMessage(Msg.makeMessage("GetNextALLLinkRecord"));
}
} catch (FieldException e) {
logger.debug("bad field handling link records {}", e.getMessage());
} catch (IOException e) {
logger.debug("got IO exception handling link records {}", e.getMessage());
} catch (IllegalStateException e) {
logger.debug("got exception requesting link records {}", e.getMessage());
} catch (InvalidMessageTypeException e) {
logger.warn("invalid message ", e);
}
}
private synchronized void done() {
isComplete = true;
logModemDB();
port.removeListener(this);
port.modemDBComplete();
}
private void logModemDB() {
try {
logger.debug("MDB ------- start of modem link records ------------------");
Map<InsteonAddress, @Nullable ModemDBEntry> dbes = port.getDriver().lockModemDBEntries();
for (Entry<InsteonAddress, @Nullable ModemDBEntry> db : dbes.entrySet()) {
List<Msg> lrs = db.getValue().getLinkRecords();
for (Msg m : lrs) {
int recordFlags = m.getByte("RecordFlags") & 0xff;
String ms = ((recordFlags & (0x1 << 6)) != 0) ? "CTRL" : "RESP";
logger.debug("MDB {}: {} group: {} data1: {} data2: {} data3: {}", db.getKey(), ms,
toHex(m.getByte("ALLLinkGroup")), toHex(m.getByte("LinkData1")),
toHex(m.getByte("LinkData2")), toHex(m.getByte("LinkData2")));
}
logger.debug("MDB -----");
}
logger.debug("MDB ---------------- end of modem link records -----------");
} catch (FieldException e) {
logger.warn("cannot access field:", e);
} finally {
port.getDriver().unlockModemDBEntries();
}
}
public static String toHex(byte b) {
return Utils.getHexString(b);
}
public void updateModemDB(InsteonAddress linkAddr, Port port, @Nullable Msg m, boolean isModem) {
try {
Map<InsteonAddress, @Nullable ModemDBEntry> dbes = port.getDriver().lockModemDBEntries();
ModemDBEntry dbe = dbes.get(linkAddr);
if (dbe == null) {
dbe = new ModemDBEntry(linkAddr, isModem);
dbes.put(linkAddr, dbe);
}
dbe.setPort(port);
if (m != null) {
dbe.addLinkRecord(m);
try {
byte group = m.getByte("ALLLinkGroup");
int recordFlags = m.getByte("RecordFlags") & 0xff;
if ((recordFlags & (0x1 << 6)) != 0) {
dbe.addControls(group);
} else {
dbe.addRespondsTo(group);
}
} catch (FieldException e) {
logger.warn("cannot access field:", e);
}
}
} finally {
port.getDriver().unlockModemDBEntries();
}
}
}

View File

@@ -0,0 +1,162 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.insteon.internal.device;
import java.lang.reflect.InvocationTargetException;
import java.util.HashMap;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.insteon.internal.message.FieldException;
import org.openhab.binding.insteon.internal.message.InvalidMessageTypeException;
import org.openhab.binding.insteon.internal.message.Msg;
import org.openhab.binding.insteon.internal.utils.Utils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A PollHandler creates an Insteon message to query a particular
* DeviceFeature of an Insteon device.
*
* @author Bernd Pfrommer - Initial contribution
* @author Rob Nielsen - Port to openHAB 2 insteon binding
*/
@NonNullByDefault
@SuppressWarnings("null")
public abstract class PollHandler {
private static final Logger logger = LoggerFactory.getLogger(PollHandler.class);
DeviceFeature feature;
Map<String, @Nullable String> parameters = new HashMap<>();
/**
* Constructor
*
* @param feature The device feature being polled
*/
PollHandler(DeviceFeature feature) {
this.feature = feature;
}
/**
* Creates Insteon message that can be used to poll a feature
* via the Insteon network.
*
* @param device reference to the insteon device to be polled
* @return Insteon query message or null if creation failed
*/
public abstract @Nullable Msg makeMsg(InsteonDevice device);
public void setParameters(Map<String, @Nullable String> hm) {
parameters = hm;
}
/**
* Returns parameter as integer
*
* @param key key of parameter
* @param def default
* @return value of parameter
*/
protected int getIntParameter(String key, int def) {
String val = parameters.get(key);
if (val == null) {
return (def); // param not found
}
int ret = def;
try {
ret = Utils.strToInt(val);
} catch (NumberFormatException e) {
logger.warn("malformed int parameter in command handler: {}", key);
}
return ret;
}
/**
* A flexible, parameterized poll handler that can generate
* most query messages. Provide the suitable parameters in
* the device features file.
*/
@NonNullByDefault
public static class FlexPollHandler extends PollHandler {
FlexPollHandler(DeviceFeature f) {
super(f);
}
@Override
public @Nullable Msg makeMsg(InsteonDevice d) {
Msg m = null;
int cmd1 = getIntParameter("cmd1", 0);
int cmd2 = getIntParameter("cmd2", 0);
int ext = getIntParameter("ext", -1);
try {
if (ext == 1 || ext == 2) {
int d1 = getIntParameter("d1", 0);
int d2 = getIntParameter("d2", 0);
int d3 = getIntParameter("d3", 0);
m = d.makeExtendedMessage((byte) 0x0f, (byte) cmd1, (byte) cmd2,
new byte[] { (byte) d1, (byte) d2, (byte) d3 });
if (ext == 1) {
m.setCRC();
} else if (ext == 2) {
m.setCRC2();
}
} else {
m = d.makeStandardMessage((byte) 0x0f, (byte) cmd1, (byte) cmd2);
}
m.setQuietTime(500L);
} catch (FieldException e) {
logger.warn("error setting field in msg: ", e);
} catch (InvalidMessageTypeException e) {
logger.warn("invalid message ", e);
}
return m;
}
}
@NonNullByDefault
public static class NoPollHandler extends PollHandler {
NoPollHandler(DeviceFeature f) {
super(f);
}
@Override
public @Nullable Msg makeMsg(InsteonDevice d) {
return null;
}
}
/**
* Factory method for creating handlers of a given name using java reflection
*
* @param ph the name of the handler to create
* @param f the feature for which to create the handler
* @return the handler which was created
*/
@Nullable
public static <T extends PollHandler> T makeHandler(@Nullable HandlerEntry ph, DeviceFeature f) {
String cname = PollHandler.class.getName() + "$" + ph.getName();
try {
Class<?> c = Class.forName(cname);
@SuppressWarnings("unchecked")
Class<? extends T> dc = (Class<? extends T>) c;
T phc = dc.getDeclaredConstructor(DeviceFeature.class).newInstance(f);
phc.setParameters(ph.getParams());
return phc;
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException | IllegalArgumentException
| InvocationTargetException | NoSuchMethodException | SecurityException e) {
logger.warn("error trying to create message handler: {}", ph.getName(), e);
}
return null;
}
}

View File

@@ -0,0 +1,205 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.insteon.internal.device;
import java.util.HashMap;
import java.util.PriorityQueue;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Class that manages all the per-device request queues using a single thread.
*
* - Each device has its own request queue, and the RequestQueueManager keeps a
* queue of queues.
* - Each entry in m_requestQueues corresponds to a single device's request queue.
* A device should never be more than once in m_requestQueues.
* - A hash map (m_requestQueueHash) is kept in sync with m_requestQueues for
* faster lookup in case a request queue is modified and needs to be
* rescheduled.
*
* @author Bernd Pfrommer - Initial contribution
* @author Rob Nielsen - Port to openHAB 2 insteon binding
*/
@NonNullByDefault
@SuppressWarnings("null")
public class RequestQueueManager {
private static @Nullable RequestQueueManager instance = null;
private final Logger logger = LoggerFactory.getLogger(RequestQueueManager.class);
private @Nullable Thread queueThread = null;
private PriorityQueue<RequestQueue> requestQueues = new PriorityQueue<>();
private HashMap<InsteonDevice, @Nullable RequestQueue> requestQueueHash = new HashMap<>();
private boolean keepRunning = true;
private RequestQueueManager() {
queueThread = new Thread(new RequestQueueReader());
queueThread.setName("Insteon Request Queue Reader");
queueThread.setDaemon(true);
queueThread.start();
}
/**
* Add device to global request queue.
*
* @param dev the device to add
* @param time the time when the queue should be processed
*/
public void addQueue(InsteonDevice dev, long time) {
synchronized (requestQueues) {
RequestQueue q = requestQueueHash.get(dev);
if (q == null) {
logger.trace("scheduling request for device {} in {} msec", dev.getAddress(),
time - System.currentTimeMillis());
q = new RequestQueue(dev, time);
} else {
logger.trace("queue for dev {} is already scheduled in {} msec", dev.getAddress(),
q.getExpirationTime() - System.currentTimeMillis());
if (!requestQueues.remove(q)) {
logger.warn("queue for {} should be there, report as bug!", dev);
}
requestQueueHash.remove(dev);
}
long expTime = q.getExpirationTime();
if (expTime > time) {
q.setExpirationTime(time);
}
// add the queue back in after (maybe) having modified
// the expiration time
requestQueues.add(q);
requestQueueHash.put(dev, q);
requestQueues.notify();
}
}
/**
* Stops request queue thread
*/
private void stopThread() {
logger.debug("stopping thread");
if (queueThread != null) {
synchronized (requestQueues) {
keepRunning = false;
requestQueues.notifyAll();
}
try {
logger.debug("waiting for thread to join");
queueThread.join();
logger.debug("request queue thread exited!");
} catch (InterruptedException e) {
logger.warn("got interrupted waiting for thread exit ", e);
}
queueThread = null;
}
}
@NonNullByDefault
class RequestQueueReader implements Runnable {
@Override
public void run() {
logger.debug("starting request queue thread");
synchronized (requestQueues) {
while (keepRunning) {
try {
while (keepRunning && !requestQueues.isEmpty()) {
RequestQueue q = requestQueues.peek();
long now = System.currentTimeMillis();
long expTime = q.getExpirationTime();
InsteonDevice dev = q.getDevice();
if (expTime > now) {
//
// The head of the queue is not up for processing yet, wait().
//
logger.trace("request queue head: {} must wait for {} msec", dev.getAddress(),
expTime - now);
requestQueues.wait(expTime - now);
//
// note that the wait() can also return because of changes to
// the queue, not just because the time expired!
//
continue;
}
//
// The head of the queue has expired and can be processed!
//
q = requestQueues.poll(); // remove front element
requestQueueHash.remove(dev); // and remove from hash map
long nextExp = dev.processRequestQueue(now);
if (nextExp > 0) {
q = new RequestQueue(dev, nextExp);
requestQueues.add(q);
requestQueueHash.put(dev, q);
logger.trace("device queue for {} rescheduled in {} msec", dev.getAddress(),
nextExp - now);
} else {
// remove from hash since queue is no longer scheduled
logger.debug("device queue for {} is empty!", dev.getAddress());
}
}
logger.trace("waiting for request queues to fill");
requestQueues.wait();
} catch (InterruptedException e) {
logger.warn("request queue thread got interrupted, breaking..", e);
break;
}
}
}
logger.debug("exiting request queue thread!");
}
}
@NonNullByDefault
public static class RequestQueue implements Comparable<RequestQueue> {
private InsteonDevice device;
private long expirationTime;
RequestQueue(InsteonDevice dev, long expirationTime) {
this.device = dev;
this.expirationTime = expirationTime;
}
public InsteonDevice getDevice() {
return device;
}
public long getExpirationTime() {
return expirationTime;
}
public void setExpirationTime(long t) {
expirationTime = t;
}
@Override
public int compareTo(RequestQueue a) {
return (int) (expirationTime - a.expirationTime);
}
}
@NonNullByDefault
public static synchronized @Nullable RequestQueueManager instance() {
if (instance == null) {
instance = new RequestQueueManager();
}
return (instance);
}
public static synchronized void destroyInstance() {
if (instance != null) {
instance.stopThread();
instance = null;
}
}
}

View File

@@ -0,0 +1,194 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.insteon.internal.device;
import java.util.HashMap;
import java.util.Map.Entry;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* This class has utilities related to the X10 protocol.
*
* @author Bernd Pfrommer - Initial contribution
* @author Rob Nielsen - Port to openHAB 2 insteon binding
*/
@NonNullByDefault
@SuppressWarnings("null")
public class X10 {
/**
* Enumerates the X10 command codes.
*
* @author Bernd Pfrommer - openHAB 1 insteonplm binding
*
*/
public enum Command {
ALL_LIGHTS_OFF(0x6),
STATUS_OFF(0xE),
ON(0x2),
PRESET_DIM_1(0xA),
ALL_LIGHTS_ON(0x1),
HAIL_ACKNOWLEDGE(0x9),
BRIGHT(0x5),
STATUS_ON(0xD),
EXTENDED_CODE(0x9),
STATUS_REQUEST(0xF),
OFF(0x3),
PRESET_DIM_2(0xB),
ALL_UNITS_OFF(0x0),
HAIL_REQUEST(0x8),
DIM(0x4),
EXTENDED_DATA(0xC);
private final byte code;
Command(int b) {
code = (byte) b;
}
public byte code() {
return code;
}
}
/**
* converts house code to clear text
*
* @param c house code as per X10 spec
* @return clear text house code, i.e letter A-P
*/
public static String houseToString(byte c) {
String s = houseCodeToString.get(c & 0xff);
return (s == null) ? "X" : s;
}
/**
* converts unit code to regular integer
*
* @param c unit code per X10 spec
* @return decoded integer, i.e. number 0-16
*/
public static int unitToInt(byte c) {
Integer i = unitCodeToInt.get(c & 0xff);
return (i == null) ? -1 : i;
}
/**
* Test if string has valid X10 address of form "H.U", e.g. A.10
*
* @param s string to test
* @return true if is valid X10 address
*/
public static boolean isValidAddress(String s) {
String[] parts = s.split("\\.");
if (parts.length != 2) {
return false;
}
return parts[0].matches("[A-P]") && parts[1].matches("\\d{1,2}");
}
/**
* Turn clear text address ("A.10") to byte code
*
* @param addr clear text address
* @return byte that encodes house + unit code
*/
public static byte addressToByte(String addr) {
String[] parts = addr.split("\\.");
int ih = houseStringToCode(parts[0]);
int iu = unitStringToCode(parts[1]);
int itot = ih << 4 | iu;
return (byte) (itot & 0xff);
}
/**
* converts String to house byte code
*
* @param s clear text house string
* @return coded house byte
*/
public static int houseStringToCode(String s) {
Integer i = findKey(houseCodeToString, s);
return (i == null) ? 0xf : i;
}
/**
* converts unit string to unit code
*
* @param s string with clear text integer inside
* @return encoded unit byte
*/
public static int unitStringToCode(String s) {
try {
Integer key = Integer.parseInt(s);
Integer i = findKey(unitCodeToInt, key);
return i;
} catch (NumberFormatException e) {
}
return 0xf;
}
private static @Nullable <T, E> T findKey(HashMap<T, E> map, E value) {
for (Entry<T, E> entry : map.entrySet()) {
if (value.equals(entry.getValue())) {
return entry.getKey();
}
}
return null;
}
/**
* Map between 4-bit X10 code and the house code.
*/
private static HashMap<Integer, @Nullable String> houseCodeToString = new HashMap<>();
/**
* Map between 4-bit X10 code and the unit code.
*/
private static HashMap<Integer, @Nullable Integer> unitCodeToInt = new HashMap<>();
static {
houseCodeToString.put(0x6, "A");
unitCodeToInt.put(0x6, 1);
houseCodeToString.put(0xe, "B");
unitCodeToInt.put(0xe, 2);
houseCodeToString.put(0x2, "C");
unitCodeToInt.put(0x2, 3);
houseCodeToString.put(0xa, "D");
unitCodeToInt.put(0xa, 4);
houseCodeToString.put(0x1, "E");
unitCodeToInt.put(0x1, 5);
houseCodeToString.put(0x9, "F");
unitCodeToInt.put(0x9, 6);
houseCodeToString.put(0x5, "G");
unitCodeToInt.put(0x5, 7);
houseCodeToString.put(0xd, "H");
unitCodeToInt.put(0xd, 8);
houseCodeToString.put(0x7, "I");
unitCodeToInt.put(0x7, 9);
houseCodeToString.put(0xf, "J");
unitCodeToInt.put(0xf, 10);
houseCodeToString.put(0x3, "K");
unitCodeToInt.put(0x3, 11);
houseCodeToString.put(0xb, "L");
unitCodeToInt.put(0xb, 12);
houseCodeToString.put(0x0, "M");
unitCodeToInt.put(0x0, 13);
houseCodeToString.put(0x8, "N");
unitCodeToInt.put(0x8, 14);
houseCodeToString.put(0x4, "O");
unitCodeToInt.put(0x4, 15);
houseCodeToString.put(0xc, "P");
unitCodeToInt.put(0xc, 16);
}
}

View File

@@ -0,0 +1,74 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.insteon.internal.discovery;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.insteon.internal.InsteonBindingConstants;
import org.openhab.binding.insteon.internal.handler.InsteonNetworkHandler;
import org.openhab.core.config.discovery.AbstractDiscoveryService;
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
import org.openhab.core.thing.ThingUID;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link InsteonDeviceDiscoveryService} is responsible for device discovery.
*
* @author Rob Nielsen - Initial contribution
*/
@NonNullByDefault
public class InsteonDeviceDiscoveryService extends AbstractDiscoveryService {
private static final String ADDRESS = "address";
private final Logger logger = LoggerFactory.getLogger(InsteonDeviceDiscoveryService.class);
public InsteonDeviceDiscoveryService(InsteonNetworkHandler handler) {
super(new HashSet<>(Arrays.asList(InsteonBindingConstants.DEVICE_THING_TYPE)), 0, false);
handler.setInsteonDeviceDiscoveryService(this);
logger.debug("Initializing InsteonNetworkDiscoveryService");
}
@Override
protected void startScan() {
}
public void addInsteonDevices(List<String> addresses, ThingUID bridgeUid) {
for (String address : addresses) {
String[] parts = address.split("\\.");
if (parts.length != 3) {
logger.warn("Address {} must be in the format XX.XX.XX", address);
continue;
}
String name = parts[0] + parts[1] + parts[2];
ThingUID uid = new ThingUID(InsteonBindingConstants.DEVICE_THING_TYPE, bridgeUid, name);
Map<String, Object> properties = new HashMap<>();
properties.put(ADDRESS, address);
thingDiscovered(
DiscoveryResultBuilder.create(uid).withProperties(properties).withLabel("Insteon Device " + name)
.withBridge(bridgeUid).withRepresentationProperty(ADDRESS).build());
logger.debug("Added Insteon device {} with the address {}", name, address);
}
}
}

View File

@@ -0,0 +1,108 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.insteon.internal.driver;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.locks.ReentrantLock;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.insteon.internal.device.InsteonAddress;
import org.openhab.binding.insteon.internal.message.Msg;
import org.openhab.binding.insteon.internal.message.MsgListener;
import org.openhab.core.io.transport.serial.SerialPortManager;
/**
* The driver class manages the modem port.
*
* @author Bernd Pfrommer - Initial contribution
* @author Rob Nielsen - Port to openHAB 2 insteon binding
*/
@NonNullByDefault
public class Driver {
private Port port;
private String portName;
private DriverListener listener;
private Map<InsteonAddress, @Nullable ModemDBEntry> modemDBEntries = new HashMap<>();
private ReentrantLock modemDBEntriesLock = new ReentrantLock();
public Driver(String portName, DriverListener listener, @Nullable SerialPortManager serialPortManager,
ScheduledExecutorService scheduler) {
this.listener = listener;
this.portName = portName;
port = new Port(portName, this, serialPortManager, scheduler);
}
public boolean isReady() {
return port.isRunning();
}
public Map<InsteonAddress, @Nullable ModemDBEntry> lockModemDBEntries() {
modemDBEntriesLock.lock();
return modemDBEntries;
}
public void unlockModemDBEntries() {
modemDBEntriesLock.unlock();
}
public void addMsgListener(MsgListener listener) {
port.addListener(listener);
}
public void removeListener(MsgListener listener) {
port.removeListener(listener);
}
public void start() {
port.start();
}
public void stop() {
port.stop();
}
public void writeMessage(Msg m) throws IOException {
port.writeMessage(m);
}
public String getPortName() {
return portName;
}
public boolean isRunning() {
return port.isRunning();
}
public boolean isMsgForUs(@Nullable InsteonAddress toAddr) {
return port.getAddress().equals(toAddr);
}
public void modemDBComplete(Port port) {
if (isModemDBComplete()) {
listener.driverCompletelyInitialized();
}
}
public boolean isModemDBComplete() {
return port.isModemDBComplete();
}
public void disconnected() {
listener.disconnected();
}
}

View File

@@ -0,0 +1,35 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.insteon.internal.driver;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Interface for classes that want to listen to notifications from
* the driver.
*
* @author Bernd Pfrommer - Initial contribution
* @author Rob Nielsen - Port to openHAB 2 insteon binding
*/
@NonNullByDefault
public interface DriverListener {
/**
* Notification that querying of the modems on all ports has successfully completed.
*/
public abstract void driverCompletelyInitialized();
/**
* Notification that the driver was disconnected
*/
public abstract void disconnected();
}

View File

@@ -0,0 +1,176 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.insteon.internal.driver;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.insteon.internal.driver.hub.HubIOStream;
import org.openhab.core.io.transport.serial.SerialPortManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Abstract class for implementation for I/O stream with anything that looks
* like a PLM (e.g. the insteon hubs, serial/usb connection etc)
*
* @author Bernd Pfrommer - Initial contribution
* @author Daniel Pfrommer - openHAB 1 insteonplm binding
* @author Rob Nielsen - Port to openHAB 2 insteon binding
*/
@NonNullByDefault
@SuppressWarnings("null")
public abstract class IOStream {
private static final Logger logger = LoggerFactory.getLogger(IOStream.class);
protected @Nullable InputStream in = null;
protected @Nullable OutputStream out = null;
private volatile boolean stopped = false;
public void start() {
stopped = false;
}
public void stop() {
stopped = true;
}
/**
* read data from iostream
*
* @param b byte array (output)
* @param offset offset for placement into byte array
* @param readSize size to read
* @return number of bytes read
*/
public int read(byte[] b, int offset, int readSize) throws InterruptedException, IOException {
int len = 0;
while (!stopped && len < 1) {
len = in.read(b, offset, readSize);
if (len == -1) {
throw new EOFException();
}
if (Thread.interrupted()) {
throw new InterruptedException();
}
}
return (len);
}
/**
* Write data to iostream
*
* @param b byte array to write
*/
public void write(byte @Nullable [] b) throws IOException {
out.write(b);
}
/**
* Opens the IOStream
*
* @return true if open was successful, false if not
*/
public abstract boolean open();
/**
* Closes the IOStream
*/
public abstract void close();
/**
* Creates an IOStream from an allowed config string:
*
* /dev/ttyXYZ (serial port like e.g. usb: /dev/ttyUSB0 or alias /dev/insteon)
*
* /hub2/user:password@myinsteonhub.mydomain.com:25105,poll_time=1000 (insteon hub2 (2014))
*
* /hub/myinsteonhub.mydomain.com:9761
*
* /tcp/serialportserver.mydomain.com:port (serial port exposed via tcp, eg. ser2net)
*
* @param config
* @return reference to IOStream
*/
public static IOStream create(@Nullable SerialPortManager serialPortManager, String config) {
if (config.startsWith("/hub2/")) {
return makeHub2014Stream(config);
} else if (config.startsWith("/hub/") || config.startsWith("/tcp/")) {
return makeTCPStream(config);
} else {
return new SerialIOStream(serialPortManager, config);
}
}
private static HubIOStream makeHub2014Stream(String config) {
@Nullable
String user = null;
@Nullable
String pass = null;
int pollTime = 1000; // poll time in milliseconds
// Get rid of the /hub2/ part and split off options at the end
String[] parts = config.substring(6).split(",");
// Parse the first part, the address
String[] adr = parts[0].split("@");
String[] hostPort;
if (adr.length > 1) {
String[] userPass = adr[0].split(":");
user = userPass[0];
pass = userPass[1];
hostPort = adr[1].split(":");
} else {
hostPort = parts[0].split(":");
}
HostPort hp = new HostPort(hostPort, 25105);
// check if additional options are given
if (parts.length > 1) {
if (parts[1].trim().startsWith("poll_time")) {
pollTime = Integer.parseInt(parts[1].split("=")[1].trim());
}
}
return new HubIOStream(hp.host, hp.port, pollTime, user, pass);
}
private static TcpIOStream makeTCPStream(String config) {
// Get rid of the /hub/ part and split off options at the end, if any
String[] parts = config.substring(5).split(",");
String[] hostPort = parts[0].split(":");
HostPort hp = new HostPort(hostPort, 9761);
return new TcpIOStream(hp.host, hp.port);
}
@NonNullByDefault
private static class HostPort {
public String host = "localhost";
public int port = -1;
HostPort(String[] hostPort, int defaultPort) {
port = defaultPort;
host = hostPort[0];
try {
if (hostPort.length > 1) {
port = Integer.parseInt(hostPort[1]);
}
} catch (NumberFormatException e) {
logger.warn("bad format for port {} ", hostPort[1], e);
}
}
}
}

View File

@@ -0,0 +1,106 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.insteon.internal.driver;
import java.util.ArrayList;
import java.util.Collections;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.insteon.internal.device.InsteonAddress;
import org.openhab.binding.insteon.internal.message.Msg;
import org.openhab.binding.insteon.internal.utils.Utils;
/**
* The ModemDBEntry class holds a modem device type record
* an xml file.
*
* @author Bernd Pfrommer - Initial contribution
* @author Rob Nielsen - Port to openHAB 2 insteon binding
*/
@NonNullByDefault
public class ModemDBEntry {
private @Nullable InsteonAddress address = null;
private boolean isModem;
private @Nullable Port port = null;
private ArrayList<Msg> linkRecords = new ArrayList<>();
private ArrayList<Byte> controls = new ArrayList<>();
private ArrayList<Byte> respondsTo = new ArrayList<>();
public ModemDBEntry(InsteonAddress aAddr, boolean isModem) {
this.address = aAddr;
this.isModem = isModem;
}
public boolean isModem() {
return isModem;
}
public ArrayList<Msg> getLinkRecords() {
return linkRecords;
}
public void addLinkRecord(Msg m) {
linkRecords.add(m);
}
public void addControls(byte c) {
controls.add(c);
}
public ArrayList<Byte> getControls() {
return controls;
}
public void addRespondsTo(byte r) {
respondsTo.add(r);
}
public ArrayList<Byte> getRespondsTo() {
return respondsTo;
}
public void setPort(Port p) {
port = p;
}
public @Nullable Port getPort() {
return port;
}
@Override
public String toString() {
String s = "addr:" + address + "|controls:[" + toGroupString(controls) + "]|responds_to:["
+ toGroupString(respondsTo) + "]|link_recors";
for (Msg m : linkRecords) {
s += ":(" + m + ")";
}
return s;
}
private String toGroupString(ArrayList<Byte> group) {
ArrayList<Byte> sorted = new ArrayList<>(group);
Collections.sort(sorted);
StringBuilder buf = new StringBuilder();
for (Byte b : sorted) {
if (buf.length() > 0) {
buf.append(",");
}
buf.append("0x");
buf.append(Utils.getHexString(b));
}
return buf.toString();
}
}

View File

@@ -0,0 +1,300 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.insteon.internal.driver;
import java.sql.Date;
import java.util.Iterator;
import java.util.SortedSet;
import java.util.TreeSet;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.insteon.internal.device.InsteonDevice;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This class manages the polling of all devices.
* Between successive polls of a any device there is a quiet time of
* at least MIN_MSEC_BETWEEN_POLLS. This avoids bunching up of poll messages
* and keeps the network bandwidth open for other messages.
*
* - An entry in the poll queue corresponds to a single device, i.e. each device should
* have exactly one entry in the poll queue. That entry is created when startPolling()
* is called, and then re-enqueued whenever it expires.
* - When a device comes up for polling, its doPoll() method is called, which in turn
* puts an entry into that devices request queue. So the Poller class actually never
* sends out messages directly. That is done by the device itself via its request
* queue. The poller just reminds the device to poll.
*
* @author Bernd Pfrommer - Initial contribution
* @author Rob Nielsen - Port to openHAB 2 insteon binding
*/
@NonNullByDefault
@SuppressWarnings("null")
public class Poller {
private static final long MIN_MSEC_BETWEEN_POLLS = 2000L;
private final Logger logger = LoggerFactory.getLogger(Poller.class);
private static Poller poller = new Poller(); // for singleton
private @Nullable Thread pollThread = null;
private TreeSet<PQEntry> pollQueue = new TreeSet<>();
private boolean keepRunning = true;
/**
* Constructor
*/
private Poller() {
}
/**
* Get size of poll queue
*
* @return number of devices being polled
*/
public int getSizeOfQueue() {
return (pollQueue.size());
}
/**
* Register a device for polling.
*
* @param d device to register for polling
* @param aNumDev approximate number of total devices
*/
public void startPolling(InsteonDevice d, int aNumDev) {
logger.debug("start polling device {}", d);
synchronized (pollQueue) {
// try to spread out the scheduling when
// starting up
int n = pollQueue.size();
long pollDelay = n * d.getPollInterval() / (aNumDev > 0 ? aNumDev : 1);
addToPollQueue(d, System.currentTimeMillis() + pollDelay);
pollQueue.notify();
}
}
/**
* Start polling a given device
*
* @param d reference to the device to be polled
*/
public void stopPolling(InsteonDevice d) {
synchronized (pollQueue) {
for (Iterator<PQEntry> i = pollQueue.iterator(); i.hasNext();) {
if (i.next().getDevice().getAddress().equals(d.getAddress())) {
i.remove();
logger.debug("stopped polling device {}", d);
}
}
}
}
/**
* Starts the poller thread
*/
public void start() {
if (pollThread == null) {
pollThread = new Thread(new PollQueueReader());
pollThread.setName("Insteon Poll Queue Reader");
pollThread.setDaemon(true);
pollThread.start();
}
}
/**
* Stops the poller thread
*/
public void stop() {
logger.debug("stopping poller!");
synchronized (pollQueue) {
pollQueue.clear();
keepRunning = false;
pollQueue.notify();
}
try {
if (pollThread != null) {
pollThread.join();
pollThread = null;
}
keepRunning = true;
} catch (InterruptedException e) {
logger.debug("got interrupted on exit: {}", e.getMessage());
}
}
/**
* Adds a device to the poll queue. After this call, the device's doPoll() method
* will be called according to the polling frequency set.
*
* @param d the device to poll periodically
* @param time the target time for the next poll to happen. Note that this time is merely
* a suggestion, and may be adjusted, because there must be at least a minimum gap in polling.
*/
private void addToPollQueue(InsteonDevice d, long time) {
long texp = findNextExpirationTime(d, time);
PQEntry ne = new PQEntry(d, texp);
logger.trace("added entry {} originally aimed at time {}", ne, String.format("%tc", new Date(time)));
pollQueue.add(ne);
}
/**
* Finds the best expiration time for a poll queue, i.e. a time slot that is after the
* desired expiration time, but does not collide with any of the already scheduled
* polls.
*
* @param d device to poll (for logging)
* @param aTime desired time after which the device should be polled
* @return the suggested time to poll
*/
private long findNextExpirationTime(InsteonDevice d, long aTime) {
long expTime = aTime;
// tailSet finds all those that expire after aTime - buffer
SortedSet<PQEntry> ts = pollQueue.tailSet(new PQEntry(d, aTime - MIN_MSEC_BETWEEN_POLLS));
if (ts.isEmpty()) {
// all entries in the poll queue are ahead of the new element,
// go ahead and simply add it to the end
expTime = aTime;
} else {
Iterator<PQEntry> pqi = ts.iterator();
PQEntry prev = pqi.next();
if (prev.getExpirationTime() > aTime + MIN_MSEC_BETWEEN_POLLS) {
// there is a time slot free before the head of the tail set
expTime = aTime;
} else {
// look for a gap where we can squeeze in
// a new poll while maintaining MIN_MSEC_BETWEEN_POLLS
while (pqi.hasNext()) {
PQEntry pqe = pqi.next();
long tcurr = pqe.getExpirationTime();
long tprev = prev.getExpirationTime();
if (tcurr - tprev >= 2 * MIN_MSEC_BETWEEN_POLLS) {
// found gap
logger.trace("dev {} time {} found slot between {} and {}", d, aTime, tprev, tcurr);
break;
}
prev = pqe;
}
expTime = prev.getExpirationTime() + MIN_MSEC_BETWEEN_POLLS;
}
}
return expTime;
}
@NonNullByDefault
private class PollQueueReader implements Runnable {
@Override
public void run() {
logger.debug("starting poll thread.");
synchronized (pollQueue) {
while (keepRunning) {
try {
readPollQueue();
} catch (InterruptedException e) {
logger.warn("poll queue reader thread interrupted!");
break;
}
}
}
logger.debug("poll thread exiting");
}
/**
* Waits for first element of poll queue to become current,
* then process it.
*
* @throws InterruptedException
*/
private void readPollQueue() throws InterruptedException {
while (pollQueue.isEmpty() && keepRunning) {
pollQueue.wait();
}
if (!keepRunning) {
return;
}
// something is in the queue
long now = System.currentTimeMillis();
PQEntry pqe = pollQueue.first();
long tfirst = pqe.getExpirationTime();
long dt = tfirst - now;
if (dt > 0) { // must wait for this item to expire
logger.trace("waiting for {} msec until {} comes due", dt, pqe);
pollQueue.wait(dt);
} else { // queue entry has expired, process it!
logger.trace("entry {} expired at time {}", pqe, now);
processQueue(now);
}
}
/**
* Takes first element off the poll queue, polls the corresponding device,
* and puts the device back into the poll queue to be polled again later.
*
* @param now the current time
*/
private void processQueue(long now) {
PQEntry pqe = pollQueue.pollFirst();
pqe.getDevice().doPoll(0);
addToPollQueue(pqe.getDevice(), now + pqe.getDevice().getPollInterval());
}
}
/**
* A poll queue entry corresponds to a single device that needs
* to be polled.
*
* @author Bernd Pfrommer - Initial contribution
*
*/
@NonNullByDefault
private static class PQEntry implements Comparable<PQEntry> {
private InsteonDevice dev;
private long expirationTime;
PQEntry(InsteonDevice dev, long time) {
this.dev = dev;
this.expirationTime = time;
}
long getExpirationTime() {
return expirationTime;
}
InsteonDevice getDevice() {
return dev;
}
@Override
public int compareTo(PQEntry b) {
return (int) (expirationTime - b.expirationTime);
}
@Override
public String toString() {
return dev.getAddress().toString() + "/" + String.format("%tc", new Date(expirationTime));
}
}
/**
* Singleton pattern instance() method
*
* @return the poller instance
*/
public static synchronized Poller instance() {
poller.start();
return (poller);
}
}

View File

@@ -0,0 +1,549 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.insteon.internal.driver;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.atomic.AtomicBoolean;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.insteon.internal.device.DeviceType;
import org.openhab.binding.insteon.internal.device.DeviceTypeLoader;
import org.openhab.binding.insteon.internal.device.InsteonAddress;
import org.openhab.binding.insteon.internal.device.InsteonDevice;
import org.openhab.binding.insteon.internal.device.ModemDBBuilder;
import org.openhab.binding.insteon.internal.handler.InsteonDeviceHandler;
import org.openhab.binding.insteon.internal.message.FieldException;
import org.openhab.binding.insteon.internal.message.InvalidMessageTypeException;
import org.openhab.binding.insteon.internal.message.Msg;
import org.openhab.binding.insteon.internal.message.MsgFactory;
import org.openhab.binding.insteon.internal.message.MsgListener;
import org.openhab.binding.insteon.internal.utils.Utils;
import org.openhab.core.io.transport.serial.SerialPortManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The Port class represents a port, that is a connection to either an Insteon modem either through
* a serial or USB port, or via an Insteon Hub.
* It does the initialization of the port, and (via its inner classes IOStreamReader and IOStreamWriter)
* manages the reading/writing of messages on the Insteon network.
*
* The IOStreamReader and IOStreamWriter class combined implement the somewhat tricky flow control protocol.
* In combination with the MsgFactory class, the incoming data stream is turned into a Msg structure
* for further processing by the upper layers (MsgListeners).
*
* A write queue is maintained to pace the flow of outgoing messages. Sending messages back-to-back
* can lead to dropped messages.
*
*
* @author Bernd Pfrommer - Initial contribution
* @author Daniel Pfrommer - openHAB 1 insteonplm binding
* @author Rob Nielsen - Port to openHAB 2 insteon binding
*/
@NonNullByDefault
@SuppressWarnings("null")
public class Port {
private final Logger logger = LoggerFactory.getLogger(Port.class);
/**
* The ReplyType is used to keep track of the state of the serial port receiver
*/
enum ReplyType {
GOT_ACK,
WAITING_FOR_ACK,
GOT_NACK
}
private IOStream ioStream;
private String devName;
private String logName;
private Modem modem;
private IOStreamReader reader;
private IOStreamWriter writer;
private final int readSize = 1024; // read buffer size
private @Nullable Thread readThread = null;
private @Nullable Thread writeThread = null;
private boolean running = false;
private boolean modemDBComplete = false;
private MsgFactory msgFactory = new MsgFactory();
private Driver driver;
private ModemDBBuilder mdbb;
private ArrayList<MsgListener> listeners = new ArrayList<>();
private LinkedBlockingQueue<Msg> writeQueue = new LinkedBlockingQueue<>();
private AtomicBoolean disconnected = new AtomicBoolean(false);
/**
* Constructor
*
* @param devName the name of the port, i.e. '/dev/insteon'
* @param d The Driver object that manages this port
*/
public Port(String devName, Driver d, @Nullable SerialPortManager serialPortManager,
ScheduledExecutorService scheduler) {
this.devName = devName;
this.driver = d;
this.logName = Utils.redactPassword(devName);
this.modem = new Modem();
addListener(modem);
this.ioStream = IOStream.create(serialPortManager, devName);
this.reader = new IOStreamReader();
this.writer = new IOStreamWriter();
this.mdbb = new ModemDBBuilder(this, scheduler);
}
public boolean isModem(InsteonAddress a) {
return modem.getAddress().equals(a);
}
public synchronized boolean isModemDBComplete() {
return (modemDBComplete);
}
public boolean isRunning() {
return running;
}
public InsteonAddress getAddress() {
return modem.getAddress();
}
public String getDeviceName() {
return devName;
}
public Driver getDriver() {
return driver;
}
public void addListener(MsgListener l) {
synchronized (listeners) {
if (!listeners.contains(l)) {
listeners.add(l);
}
}
}
public void removeListener(MsgListener l) {
synchronized (listeners) {
if (listeners.remove(l)) {
logger.debug("removed listener from port");
}
}
}
/**
* Clear modem database that has been queried so far.
*/
public void clearModemDB() {
logger.debug("clearing modem db!");
Map<InsteonAddress, @Nullable ModemDBEntry> dbes = getDriver().lockModemDBEntries();
for (InsteonAddress addr : dbes.keySet()) {
if (!dbes.get(addr).isModem()) {
dbes.remove(addr);
}
}
getDriver().unlockModemDBEntries();
}
/**
* Starts threads necessary for reading and writing
*/
public void start() {
logger.debug("starting port {}", logName);
if (running) {
logger.debug("port {} already running, not started again", logName);
return;
}
writeQueue.clear();
if (!ioStream.open()) {
logger.debug("failed to open port {}", logName);
return;
}
ioStream.start();
readThread = new Thread(reader);
readThread.setName("Insteon " + logName + " Reader");
readThread.setDaemon(true);
readThread.start();
writeThread = new Thread(writer);
writeThread.setName("Insteon " + logName + " Writer");
writeThread.setDaemon(true);
writeThread.start();
if (!mdbb.isComplete()) {
modem.initialize();
mdbb.start(); // start downloading the device list
}
running = true;
disconnected.set(false);
}
/**
* Stops all threads
*/
public void stop() {
if (!running) {
logger.debug("port {} not running, no need to stop it", logName);
return;
}
running = false;
ioStream.stop();
ioStream.close();
if (readThread != null) {
readThread.interrupt();
}
if (writeThread != null) {
writeThread.interrupt();
}
logger.debug("waiting for read thread to exit for port {}", logName);
try {
if (readThread != null) {
readThread.join();
}
} catch (InterruptedException e) {
logger.debug("got interrupted waiting for read thread to exit.");
}
logger.debug("waiting for write thread to exit for port {}", logName);
try {
if (writeThread != null) {
writeThread.join();
}
} catch (InterruptedException e) {
logger.debug("got interrupted waiting for write thread to exit.");
}
readThread = null;
writeThread = null;
logger.debug("all threads for port {} stopped.", logName);
}
/**
* Adds message to the write queue
*
* @param m message to be added to the write queue
* @throws IOException
*/
public void writeMessage(@Nullable Msg m) throws IOException {
if (m == null) {
logger.warn("trying to write null message!");
throw new IOException("trying to write null message!");
}
if (m.getData() == null) {
logger.warn("trying to write message without data!");
throw new IOException("trying to write message without data!");
}
try {
writeQueue.add(m);
logger.trace("enqueued msg: {}", m);
} catch (IllegalStateException e) {
logger.warn("cannot write message {}, write queue is full!", m);
}
}
/**
* Gets called by the modem database builder when the modem database is complete
*/
public void modemDBComplete() {
synchronized (this) {
modemDBComplete = true;
}
driver.modemDBComplete(this);
}
public void disconnected() {
if (isRunning()) {
if (!disconnected.getAndSet(true)) {
logger.warn("port {} disconnected", logName);
driver.disconnected();
}
}
}
/**
* The IOStreamReader uses the MsgFactory to turn the incoming bytes into
* Msgs for the listeners. It also communicates with the IOStreamWriter
* to implement flow control (tell the IOStreamWriter that it needs to retransmit,
* or the reply message has been received correctly).
*
* @author Bernd Pfrommer - Initial contribution
*/
@NonNullByDefault
class IOStreamReader implements Runnable {
private ReplyType reply = ReplyType.GOT_ACK;
private Object replyLock = new Object();
private boolean dropRandomBytes = false; // set to true for fault injection
/**
* Helper function for implementing synchronization between reader and writer
*
* @return reference to the RequesReplyLock
*/
public Object getRequestReplyLock() {
return replyLock;
}
@Override
public void run() {
logger.debug("starting reader...");
byte[] buffer = new byte[2 * readSize];
Random rng = new Random();
try {
for (int len = -1; (len = ioStream.read(buffer, 0, readSize)) > 0;) {
if (dropRandomBytes && rng.nextInt(100) < 20) {
len = dropBytes(buffer, len);
}
msgFactory.addData(buffer, len);
processMessages();
}
} catch (InterruptedException e) {
logger.debug("reader thread got interrupted!");
} catch (IOException e) {
logger.debug("got an io exception in the reader thread");
disconnected();
}
logger.debug("reader thread exiting!");
}
private void processMessages() {
// must call processData() until msgFactory done fully processing buffer
while (!msgFactory.isDone()) {
try {
Msg msg = msgFactory.processData();
if (msg != null) {
toAllListeners(msg);
notifyWriter(msg);
}
} catch (IOException e) {
// got bad data from modem,
// unblock those waiting for ack
synchronized (getRequestReplyLock()) {
if (reply == ReplyType.WAITING_FOR_ACK) {
logger.debug("got bad data back, must assume message was acked.");
reply = ReplyType.GOT_ACK;
getRequestReplyLock().notify();
}
}
}
}
}
private void notifyWriter(Msg msg) {
synchronized (getRequestReplyLock()) {
if (reply == ReplyType.WAITING_FOR_ACK) {
if (!msg.isUnsolicited()) {
reply = (msg.isPureNack() ? ReplyType.GOT_NACK : ReplyType.GOT_ACK);
logger.trace("signaling receipt of ack: {}", (reply == ReplyType.GOT_ACK));
getRequestReplyLock().notify();
} else if (msg.isPureNack()) {
reply = ReplyType.GOT_NACK;
logger.trace("signaling receipt of pure nack");
getRequestReplyLock().notify();
} else {
logger.trace("got unsolicited message");
}
}
}
}
/**
* Drops bytes randomly from buffer to simulate errors seen
* from the InsteonHub using the raw interface
*
* @param buffer byte buffer from which to drop bytes
* @param len original number of valid bytes in buffer
* @return length of byte buffer after dropping from it
*/
private int dropBytes(byte[] buffer, int len) {
final int dropRate = 2; // in percent
Random rng = new Random();
ArrayList<Byte> l = new ArrayList<>();
for (int i = 0; i < len; i++) {
if (rng.nextInt(100) >= dropRate) {
l.add(buffer[i]);
}
}
for (int i = 0; i < l.size(); i++) {
buffer[i] = l.get(i);
}
return (l.size());
}
@SuppressWarnings("unchecked")
private void toAllListeners(Msg msg) {
// When we deliver the message, the recipient
// may in turn call removeListener() or addListener(),
// thereby corrupting the very same list we are iterating
// through. That's why we make a copy of it, and
// iterate through the copy.
ArrayList<MsgListener> tempList = null;
synchronized (listeners) {
tempList = (ArrayList<MsgListener>) listeners.clone();
}
for (MsgListener l : tempList) {
l.msg(msg); // deliver msg to listener
}
}
/**
* Blocking wait for ack or nack from modem.
* Called by IOStreamWriter for flow control.
*
* @return true if retransmission is necessary
*/
public boolean waitForReply() {
reply = ReplyType.WAITING_FOR_ACK;
while (reply == ReplyType.WAITING_FOR_ACK) {
try {
logger.trace("writer waiting for ack.");
// There have been cases observed, in particular for
// the Hub, where we get no ack or nack back, causing the binding
// to hang in the wait() below, because unsolicited messages
// do not trigger a notify(). For this reason we request retransmission
// if the wait() times out.
getRequestReplyLock().wait(30000); // be patient for 30 msec
if (reply == ReplyType.WAITING_FOR_ACK) { // timeout expired without getting ACK or NACK
logger.trace("writer timeout expired, asking for retransmit!");
reply = ReplyType.GOT_NACK;
break;
} else {
logger.trace("writer got ack: {}", (reply == ReplyType.GOT_ACK));
}
} catch (InterruptedException e) {
break; // done for the day...
}
}
return (reply == ReplyType.GOT_NACK);
}
}
/**
* Writes messages to the port. Flow control is implemented following Insteon
* documents to avoid over running the modem.
*
* @author Bernd Pfrommer - Initial contribution
*/
@NonNullByDefault
class IOStreamWriter implements Runnable {
private static final int WAIT_TIME = 200; // milliseconds
@Override
public void run() {
logger.debug("starting writer...");
while (true) {
try {
// this call blocks until the lock on the queue is released
logger.trace("writer checking message queue");
Msg msg = writeQueue.take();
if (msg.getData() == null) {
logger.warn("found null message in write queue!");
} else {
logger.debug("writing ({}): {}", msg.getQuietTime(), msg);
// To debug race conditions during startup (i.e. make the .items
// file definitions be available *before* the modem link records,
// slow down the modem traffic with the following statement:
// Thread.sleep(500);
synchronized (reader.getRequestReplyLock()) {
ioStream.write(msg.getData());
while (reader.waitForReply()) {
Thread.sleep(WAIT_TIME);
logger.trace("retransmitting msg: {}", msg);
ioStream.write(msg.getData());
}
}
// if rate limited, need to sleep now.
if (msg.getQuietTime() > 0) {
Thread.sleep(msg.getQuietTime());
}
}
} catch (InterruptedException e) {
logger.debug("got interrupted exception in write thread");
break;
} catch (IOException e) {
logger.debug("got an io exception in the write thread");
disconnected();
break;
}
}
logger.debug("writer thread exiting!");
}
}
/**
* Class to get info about the modem
*/
@NonNullByDefault
class Modem implements MsgListener {
private @Nullable InsteonDevice device = null;
InsteonAddress getAddress() {
return (device == null) ? new InsteonAddress() : (device.getAddress());
}
@Nullable
InsteonDevice getDevice() {
return device;
}
@Override
public void msg(Msg msg) {
try {
if (msg.isPureNack()) {
return;
}
if (msg.getByte("Cmd") == 0x60) {
// add the modem to the device list
InsteonAddress a = new InsteonAddress(msg.getAddress("IMAddress"));
DeviceType dt = DeviceTypeLoader.instance().getDeviceType(InsteonDeviceHandler.PLM_PRODUCT_KEY);
if (dt == null) {
logger.warn("unknown modem product key: {} for modem: {}.",
InsteonDeviceHandler.PLM_PRODUCT_KEY, a);
} else {
device = InsteonDevice.makeDevice(dt);
device.setAddress(a);
device.setProductKey(InsteonDeviceHandler.PLM_PRODUCT_KEY);
device.setDriver(driver);
device.setIsModem(true);
logger.debug("found modem {} in device_types: {}", a, device.toString());
mdbb.updateModemDB(a, Port.this, null, true);
}
// can unsubscribe now
removeListener(this);
}
} catch (FieldException e) {
logger.warn("error parsing im info reply field: ", e);
}
}
public void initialize() {
try {
Msg m = Msg.makeMessage("GetIMInfo");
writeMessage(m);
} catch (IOException e) {
logger.warn("modem init failed!", e);
} catch (InvalidMessageTypeException e) {
logger.warn("invalid message", e);
}
}
}
}

View File

@@ -0,0 +1,137 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.insteon.internal.driver;
import java.io.IOException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.io.transport.serial.PortInUseException;
import org.openhab.core.io.transport.serial.SerialPort;
import org.openhab.core.io.transport.serial.SerialPortIdentifier;
import org.openhab.core.io.transport.serial.SerialPortManager;
import org.openhab.core.io.transport.serial.UnsupportedCommOperationException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Implements IOStream for serial devices.
*
* @author Bernd Pfrommer - Initial contribution
* @author Daniel Pfrommer - openHAB 1 insteonplm binding
* @author Rob Nielsen - Port to openHAB 2 insteon binding
*/
@NonNullByDefault
@SuppressWarnings("null")
public class SerialIOStream extends IOStream {
private final Logger logger = LoggerFactory.getLogger(SerialIOStream.class);
private @Nullable SerialPort port = null;
private final String appName = "PLM";
private int baudRate = 19200;
private String devName;
private boolean validConfig = true;
private @Nullable SerialPortManager serialPortManager;
public SerialIOStream(@Nullable SerialPortManager serialPortManager, String config) {
this.serialPortManager = serialPortManager;
String[] parts = config.split(",");
devName = parts[0];
for (int i = 1; i < parts.length; i++) {
String parameter = parts[i];
String[] paramParts = parameter.split("=");
if (paramParts.length != 2) {
logger.warn("{} invalid parameter format '{}', must be 'key=value'.", config, parameter);
validConfig = false;
} else {
String key = paramParts[0];
String value = paramParts[1];
if (key.equals("baudRate")) {
try {
baudRate = Integer.parseInt(value);
} catch (NumberFormatException e) {
logger.warn("{} baudRate {} must be an integer.", config, value);
validConfig = false;
}
} else {
logger.warn("{} invalid parameter '{}'.", config, parameter);
validConfig = false;
}
}
}
}
@Override
public boolean open() {
if (!validConfig) {
logger.warn("{} has an invalid configuration.", devName);
return false;
}
try {
SerialPortIdentifier spi = serialPortManager.getIdentifier(devName);
if (spi == null) {
logger.warn("{} is not a valid serial port.", devName);
return false;
}
port = spi.open(appName, 1000);
port.setSerialPortParams(baudRate, SerialPort.DATABITS_8, SerialPort.STOPBITS_1, SerialPort.PARITY_NONE);
port.setFlowControlMode(SerialPort.FLOWCONTROL_NONE);
logger.debug("setting {} baud rate to {}", devName, baudRate);
port.enableReceiveThreshold(1);
port.enableReceiveTimeout(1000);
in = port.getInputStream();
out = port.getOutputStream();
logger.debug("successfully opened port {}", devName);
return true;
} catch (IOException e) {
logger.warn("cannot open port: {}, got IOException {}", devName, e.getMessage());
} catch (PortInUseException e) {
logger.warn("cannot open port: {}, it is in use!", devName);
} catch (UnsupportedCommOperationException e) {
logger.warn("got unsupported operation {} on port {}", e.getMessage(), devName);
}
return false;
}
@Override
public void close() {
if (in != null) {
try {
in.close();
} catch (IOException e) {
logger.warn("failed to close input stream", e);
}
in = null;
}
if (out != null) {
try {
out.close();
} catch (IOException e) {
logger.warn("failed to close output stream", e);
}
out = null;
}
if (port != null) {
port.close();
port = null;
}
}
}

View File

@@ -0,0 +1,101 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.insteon.internal.driver;
import java.io.IOException;
import java.net.Socket;
import java.net.UnknownHostException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Implements IOStream for the older hubs (pre 2014).
* Also works for serial ports exposed via tcp, eg. ser2net
*
* @author Bernd Pfrommer - Initial contribution
* @author Rob Nielsen - Port to openHAB 2 insteon binding
*
*/
@NonNullByDefault
@SuppressWarnings("null")
public class TcpIOStream extends IOStream {
private final Logger logger = LoggerFactory.getLogger(TcpIOStream.class);
private @Nullable String host = null;
private int port = -1;
private @Nullable Socket socket = null;
/**
* Constructor
*
* @param host host name of hub device
* @param port port to connect to
*/
public TcpIOStream(String host, int port) {
this.host = host;
this.port = port;
}
@Override
public boolean open() {
if (host == null || port < 0) {
logger.warn("tcp connection to hub not properly configured!");
return (false);
}
try {
socket = new Socket(host, port);
in = socket.getInputStream();
out = socket.getOutputStream();
} catch (UnknownHostException e) {
logger.warn("unknown host name: {}", host);
return (false);
} catch (IOException e) {
logger.warn("cannot open connection to {} port {}: {}", host, port, e.getMessage());
return (false);
}
return true;
}
@Override
public void close() {
if (in != null) {
try {
in.close();
} catch (IOException e) {
logger.warn("failed to close input stream", e);
}
in = null;
}
if (out != null) {
try {
out.close();
} catch (IOException e) {
logger.warn("failed to close output stream", e);
}
out = null;
}
if (socket != null) {
try {
socket.close();
} catch (IOException e) {
logger.warn("failed to close the socket", e);
}
socket = null;
}
}
}

View File

@@ -0,0 +1,407 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.insteon.internal.driver.hub;
import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import org.apache.commons.lang.StringUtils;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.insteon.internal.driver.IOStream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Implements IOStream for a Hub 2014 device
*
* @author Daniel Pfrommer - Initial contribution
* @author Rob Nielsen - Port to openHAB 2 insteon binding
*
*/
@NonNullByDefault
@SuppressWarnings("null")
public class HubIOStream extends IOStream implements Runnable {
private final Logger logger = LoggerFactory.getLogger(HubIOStream.class);
private static final String BS_START = "<BS>";
private static final String BS_END = "</BS>";
/** time between polls (in milliseconds */
private int pollTime = 1000;
private String baseUrl;
private @Nullable String auth = null;
private @Nullable Thread pollThread = null;
// index of the last byte we have read in the buffer
private int bufferIdx = -1;
private boolean polling;
/**
* Constructor for HubIOStream
*
* @param host host name of hub device
* @param port port to connect to
* @param pollTime time between polls (in milliseconds)
* @param user hub user name
* @param pass hub password
*/
public HubIOStream(String host, int port, int pollTime, @Nullable String user, @Nullable String pass) {
this.pollTime = pollTime;
StringBuilder s = new StringBuilder();
s.append("http://");
s.append(host);
if (port != -1) {
s.append(":").append(port);
}
baseUrl = s.toString();
if (user != null && pass != null) {
auth = "Basic " + Base64.getEncoder().encodeToString((user + ":" + pass).getBytes(StandardCharsets.UTF_8));
}
}
@Override
public boolean open() {
try {
clearBuffer();
} catch (IOException e) {
logger.warn("open failed: {}", e.getMessage());
return false;
}
in = new HubInputStream();
out = new HubOutputStream();
polling = true;
pollThread = new Thread(this);
pollThread.setName("Insteon Hub Poller");
pollThread.setDaemon(true);
pollThread.start();
return true;
}
@Override
public void close() {
polling = false;
if (pollThread != null) {
pollThread = null;
}
if (in != null) {
try {
in.close();
} catch (IOException e) {
logger.warn("failed to close input stream", e);
}
in = null;
}
if (out != null) {
try {
out.close();
} catch (IOException e) {
logger.warn("failed to close output stream", e);
}
out = null;
}
}
/**
* Fetches the latest status buffer from the Hub
*
* @return string with status buffer
* @throws IOException
*/
private synchronized String bufferStatus() throws IOException {
String result = getURL("/buffstatus.xml");
int start = result.indexOf(BS_START);
if (start == -1) {
throw new IOException("malformed bufferstatus.xml");
}
start += BS_START.length();
int end = result.indexOf(BS_END, start);
if (end == -1) {
throw new IOException("malformed bufferstatus.xml");
}
return result.substring(start, end).trim();
}
/**
* Sends command to Hub to clear the status buffer
*
* @throws IOException
*/
private synchronized void clearBuffer() throws IOException {
logger.trace("clearing buffer");
getURL("/1?XB=M=1");
bufferIdx = 0;
}
/**
* Sends Insteon message (byte array) as a readable ascii string to the Hub
*
* @param msg byte array representing the Insteon message
* @throws IOException in case of I/O error
*/
public synchronized void write(ByteBuffer msg) throws IOException {
poll(); // fetch the status buffer before we send out commands
StringBuilder b = new StringBuilder();
while (msg.remaining() > 0) {
b.append(String.format("%02x", msg.get()));
}
String hexMSG = b.toString();
logger.trace("writing a message");
getURL("/3?" + hexMSG + "=I=3");
bufferIdx = 0;
}
/**
* Polls the Hub web interface to fetch the status buffer
*
* @throws IOException if something goes wrong with I/O
*/
public synchronized void poll() throws IOException {
String buffer = bufferStatus(); // fetch via http call
logger.trace("poll: {}", buffer);
//
// The Hub maintains a ring buffer where the last two digits (in hex!) represent
// the position of the last byte read.
//
String data = buffer.substring(0, buffer.length() - 2); // pure data w/o index pointer
int nIdx = -1;
try {
nIdx = Integer.parseInt(buffer.substring(buffer.length() - 2, buffer.length()), 16);
} catch (NumberFormatException e) {
bufferIdx = -1;
logger.warn("invalid buffer size received in line: {}", buffer);
return;
}
if (bufferIdx == -1) {
// this is the first call or first call after error, no need for buffer copying
bufferIdx = nIdx;
return; // XXX why return here????
}
if (StringUtils.repeat("0", data.length()).equals(data)) {
logger.trace("skip cleared buffer");
bufferIdx = 0;
return;
}
StringBuilder msg = new StringBuilder();
if (nIdx < bufferIdx) {
String msgStart = data.substring(bufferIdx, data.length());
String msgEnd = data.substring(0, nIdx);
if (StringUtils.repeat("0", msgStart.length()).equals(msgStart)) {
logger.trace("discard cleared buffer wrap around msg start");
msgStart = "";
}
msg.append(msgStart + msgEnd);
logger.trace("wrap around: copying new data on: {}", msg.toString());
} else {
msg.append(data.substring(bufferIdx, nIdx));
logger.trace("no wrap: appending new data: {}", msg.toString());
}
if (msg.length() != 0) {
ByteBuffer buf = ByteBuffer.wrap(hexStringToByteArray(msg.toString()));
((HubInputStream) in).handle(buf);
}
bufferIdx = nIdx;
}
/**
* Helper method to fetch url from http server
*
* @param resource the url
* @return contents returned by http server
* @throws IOException
*/
private String getURL(String resource) throws IOException {
String url = baseUrl + resource;
HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
try {
connection.setConnectTimeout(30000);
connection.setUseCaches(false);
connection.setDoInput(true);
connection.setDoOutput(false);
if (auth != null) {
connection.setRequestProperty("Authorization", auth);
}
logger.debug("getting {}", url);
int responseCode = connection.getResponseCode();
if (responseCode != 200) {
if (responseCode == 401) {
logger.warn(
"Bad username or password. See the label on the bottom of the hub for the correct login information.");
throw new IOException("login credentials are incorrect");
} else {
String message = url + " failed with the response code: " + responseCode;
logger.warn(message);
throw new IOException(message);
}
}
return getData(connection.getInputStream());
} finally {
connection.disconnect();
}
}
private String getData(InputStream is) throws IOException {
BufferedInputStream bis = new BufferedInputStream(is);
try {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int length = 0;
while ((length = bis.read(buffer)) != -1) {
baos.write(buffer, 0, length);
}
String s = baos.toString();
return s;
} finally {
bis.close();
}
}
/**
* Entry point for thread
*/
@Override
public void run() {
while (polling) {
try {
poll();
} catch (IOException e) {
logger.warn("got exception while polling: {}", e.toString());
}
try {
Thread.sleep(pollTime);
} catch (InterruptedException e) {
break;
}
}
}
/**
* Helper function to convert an ascii hex string (received from hub)
* into a byte array
*
* @param s string received from hub
* @return simple byte array
*/
public static byte[] hexStringToByteArray(String s) {
int len = s.length();
byte[] bytes = new byte[len / 2];
for (int i = 0; i < len; i += 2) {
bytes[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4) + Character.digit(s.charAt(i + 1), 16));
}
return bytes;
}
/**
* Implements an InputStream for the Hub 2014
*
* @author Daniel Pfrommer - Initial contribution
*
*/
@NonNullByDefault
public class HubInputStream extends InputStream {
// A buffer to keep bytes while we are waiting for the inputstream to read
private ReadByteBuffer buffer = new ReadByteBuffer(1024);
public HubInputStream() {
}
public void handle(ByteBuffer b) throws IOException {
// Make sure we cleanup as much space as possible
buffer.makeCompact();
buffer.add(b.array());
}
@Override
public int read() throws IOException {
return buffer.get();
}
@Override
public int read(byte @Nullable [] b, int off, int len) throws IOException {
return buffer.get(b, off, len);
}
@Override
public void close() throws IOException {
buffer.done();
}
}
/**
* Implements an OutputStream for the Hub 2014
*
* @author Daniel Pfrommer - Initial contribution
*
*/
@NonNullByDefault
public class HubOutputStream extends OutputStream {
private ByteArrayOutputStream out = new ByteArrayOutputStream();
@Override
public void write(int b) {
out.write(b);
flushBuffer();
}
@Override
public void write(byte @Nullable [] b, int off, int len) {
out.write(b, off, len);
flushBuffer();
}
private void flushBuffer() {
ByteBuffer buffer = ByteBuffer.wrap(out.toByteArray());
try {
HubIOStream.this.write(buffer);
} catch (IOException e) {
logger.warn("failed to write to hub: {}", e.toString());
}
out.reset();
}
}
}

View File

@@ -0,0 +1,156 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.insteon.internal.driver.hub;
import java.io.IOException;
import java.util.Arrays;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* ReadByteBuffer buffer class
*
* @author Daniel Pfrommer - Initial contribution
* @author Rob Nielsen - Port to openHAB 2 insteon binding
*/
@NonNullByDefault
public class ReadByteBuffer {
private byte buf[]; // the actual buffer
private int count; // number of valid bytes
private int index = 0; // current read index
private boolean done = false;
/**
* Constructor for ByteArrayIO with dynamic size
*
* @param size initial size, but will grow dynamically
*/
public ReadByteBuffer(int size) {
this.buf = new byte[size];
}
/**
* Done reading bytes
*/
public synchronized void done() {
done = true;
notifyAll();
}
/**
* Number of unread bytes
*
* @return number of bytes not yet read
*/
public synchronized int remaining() {
return count - index;
}
/**
* Blocking read of a single byte
*
* @return byte read
* @throws IOException
*/
public synchronized byte get() throws IOException {
while (!done && remaining() < 1) {
try {
wait();
} catch (InterruptedException e) {
throw new IOException("interrupted");
}
}
if (done) {
throw new IOException("done");
}
return buf[index++];
}
/**
* Blocking read of multiple bytes
*
* @param bytes destination array for bytes read
* @param off offset into dest array
* @param len max number of bytes to read into dest array
* @return number of bytes actually read
* @throws IOException
*/
public synchronized int get(byte @Nullable [] bytes, int off, int len) throws IOException {
while (!done && remaining() < 1) {
try {
wait();
} catch (InterruptedException e) {
throw new IOException("interrupted");
}
}
if (done) {
throw new IOException("done");
}
int b = Math.min(len, remaining());
System.arraycopy(buf, index, bytes, off, b);
index += b;
return b;
}
/**
* Adds bytes to the byte buffer
*
* @param b byte array with new bytes
* @param off starting offset into buffer
* @param len number of bytes to add
*/
private synchronized void add(byte b[], int off, int len) {
if ((off < 0) || (off > b.length) || (len < 0) || ((off + len) > b.length) || ((off + len) < 0)) {
throw new IndexOutOfBoundsException();
} else if (len == 0) {
return;
}
int nCount = count + len;
if (nCount > buf.length) {
// dynamically grow the array
buf = Arrays.copyOf(buf, Math.max(buf.length << 1, nCount));
}
// append new data to end of buffer
System.arraycopy(b, off, buf, count, len);
count = nCount;
notifyAll();
}
/**
* Adds bytes to the byte buffer
*
* @param b the new bytes to be added
*/
public void add(byte[] b) {
add(b, 0, b.length);
}
/**
* Shrink the buffer to smallest size possible
*/
public synchronized void makeCompact() {
if (index == 0) {
return;
}
byte[] newBuf = new byte[remaining()];
System.arraycopy(buf, index, newBuf, 0, newBuf.length);
index = 0;
count = newBuf.length;
buf = newBuf;
}
}

View File

@@ -0,0 +1,461 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
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;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.insteon.internal.InsteonBinding;
import org.openhab.binding.insteon.internal.InsteonBindingConstants;
import org.openhab.binding.insteon.internal.config.InsteonChannelConfiguration;
import org.openhab.binding.insteon.internal.config.InsteonDeviceConfiguration;
import org.openhab.binding.insteon.internal.device.DeviceFeature;
import org.openhab.binding.insteon.internal.device.DeviceTypeLoader;
import org.openhab.binding.insteon.internal.device.InsteonAddress;
import org.openhab.binding.insteon.internal.device.InsteonDevice;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.binding.BaseThingHandler;
import org.openhab.core.thing.type.ChannelTypeUID;
import org.openhab.core.types.Command;
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.
*
* @author Rob Nielsen - Initial contribution
*/
@NonNullByDefault
@SuppressWarnings("null")
public class InsteonDeviceHandler extends BaseThingHandler {
private static final Set<String> ALL_CHANNEL_IDS = Collections.unmodifiableSet(Stream.of(
InsteonBindingConstants.AC_DELAY, InsteonBindingConstants.BACKLIGHT_DURATION,
InsteonBindingConstants.BATTERY_LEVEL, InsteonBindingConstants.BATTERY_PERCENT,
InsteonBindingConstants.BATTERY_WATERMARK_LEVEL, InsteonBindingConstants.BEEP,
InsteonBindingConstants.BOTTOM_OUTLET, InsteonBindingConstants.BUTTON_A, InsteonBindingConstants.BUTTON_B,
InsteonBindingConstants.BUTTON_C, InsteonBindingConstants.BUTTON_D, InsteonBindingConstants.BUTTON_E,
InsteonBindingConstants.BUTTON_F, InsteonBindingConstants.BUTTON_G, InsteonBindingConstants.BUTTON_H,
InsteonBindingConstants.BROADCAST_ON_OFF, InsteonBindingConstants.CONTACT,
InsteonBindingConstants.COOL_SET_POINT, InsteonBindingConstants.DIMMER, InsteonBindingConstants.FAN,
InsteonBindingConstants.FAN_MODE, InsteonBindingConstants.FAST_ON_OFF,
InsteonBindingConstants.FAST_ON_OFF_BUTTON_A, InsteonBindingConstants.FAST_ON_OFF_BUTTON_B,
InsteonBindingConstants.FAST_ON_OFF_BUTTON_C, InsteonBindingConstants.FAST_ON_OFF_BUTTON_D,
InsteonBindingConstants.FAST_ON_OFF_BUTTON_E, InsteonBindingConstants.FAST_ON_OFF_BUTTON_F,
InsteonBindingConstants.FAST_ON_OFF_BUTTON_G, InsteonBindingConstants.FAST_ON_OFF_BUTTON_H,
InsteonBindingConstants.HEAT_SET_POINT, InsteonBindingConstants.HUMIDITY,
InsteonBindingConstants.HUMIDITY_HIGH, InsteonBindingConstants.HUMIDITY_LOW,
InsteonBindingConstants.IS_COOLING, InsteonBindingConstants.IS_HEATING,
InsteonBindingConstants.KEYPAD_BUTTON_A, InsteonBindingConstants.KEYPAD_BUTTON_B,
InsteonBindingConstants.KEYPAD_BUTTON_C, InsteonBindingConstants.KEYPAD_BUTTON_D,
InsteonBindingConstants.KEYPAD_BUTTON_E, InsteonBindingConstants.KEYPAD_BUTTON_F,
InsteonBindingConstants.KEYPAD_BUTTON_G, InsteonBindingConstants.KEYPAD_BUTTON_H,
InsteonBindingConstants.KWH, InsteonBindingConstants.LAST_HEARD_FROM,
InsteonBindingConstants.LED_BRIGHTNESS, InsteonBindingConstants.LED_ONOFF,
InsteonBindingConstants.LIGHT_DIMMER, InsteonBindingConstants.LIGHT_LEVEL,
InsteonBindingConstants.LIGHT_LEVEL_ABOVE_THRESHOLD, InsteonBindingConstants.LOAD_DIMMER,
InsteonBindingConstants.LOAD_SWITCH, InsteonBindingConstants.LOAD_SWITCH_FAST_ON_OFF,
InsteonBindingConstants.LOAD_SWITCH_MANUAL_CHANGE, InsteonBindingConstants.LOWBATTERY,
InsteonBindingConstants.MANUAL_CHANGE, InsteonBindingConstants.MANUAL_CHANGE_BUTTON_A,
InsteonBindingConstants.MANUAL_CHANGE_BUTTON_B, InsteonBindingConstants.MANUAL_CHANGE_BUTTON_C,
InsteonBindingConstants.MANUAL_CHANGE_BUTTON_D, InsteonBindingConstants.MANUAL_CHANGE_BUTTON_E,
InsteonBindingConstants.MANUAL_CHANGE_BUTTON_F, InsteonBindingConstants.MANUAL_CHANGE_BUTTON_G,
InsteonBindingConstants.MANUAL_CHANGE_BUTTON_H, InsteonBindingConstants.NOTIFICATION,
InsteonBindingConstants.ON_LEVEL, InsteonBindingConstants.RAMP_DIMMER, InsteonBindingConstants.RAMP_RATE,
InsteonBindingConstants.RESET, InsteonBindingConstants.STAGE1_DURATION, InsteonBindingConstants.SWITCH,
InsteonBindingConstants.SYSTEM_MODE, InsteonBindingConstants.TAMPER_SWITCH,
InsteonBindingConstants.TEMPERATURE, InsteonBindingConstants.TEMPERATURE_LEVEL,
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";
public static final String CMD_UPDATE = "update";
public static final String DATA = "data";
public static final String FIELD = "field";
public static final String FIELD_BATTERY_LEVEL = "battery_level";
public static final String FIELD_BATTERY_PERCENTAGE = "battery_percentage";
public static final String FIELD_BATTERY_WATERMARK_LEVEL = "battery_watermark_level";
public static final String FIELD_KWH = "kwh";
public static final String FIELD_LIGHT_LEVEL = "light_level";
public static final String FIELD_TEMPERATURE_LEVEL = "temperature_level";
public static final String FIELD_WATTS = "watts";
public static final String GROUP = "group";
public static final String METER = "meter";
public static final String HIDDEN_DOOR_SENSOR_PRODUCT_KEY = "F00.00.03";
public static final String MOTION_SENSOR_II_PRODUCT_KEY = "F00.00.24";
public static final String MOTION_SENSOR_PRODUCT_KEY = "0x00004A";
public static final String PLM_PRODUCT_KEY = "0x000045";
public static final String POWER_METER_PRODUCT_KEY = "F00.00.17";
private final Logger logger = LoggerFactory.getLogger(InsteonDeviceHandler.class);
private @Nullable InsteonDeviceConfiguration config;
public InsteonDeviceHandler(Thing thing) {
super(thing);
}
@Override
public void initialize() {
config = getConfigAs(InsteonDeviceConfiguration.class);
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;
}
String address = config.getAddress();
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("{} {}", thing.getUID().getAsString(), msg);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, msg);
return;
}
String productKey = config.getProductKey();
if (DeviceTypeLoader.instance().getDeviceType(productKey) == null) {
String msg = "Unable to start Insteon device, invalid product key '" + productKey + "'.";
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("{} {}", thing.getUID().getAsString(), msg);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, msg);
return;
}
InsteonDevice device = insteonBinding.makeNewDevice(insteonAddress, productKey, deviceConfigMap);
StringBuilder channelList = new StringBuilder();
List<Channel> channels = new ArrayList<>();
String thingId = getThing().getUID().getAsString();
for (String channelId : ALL_CHANNEL_IDS) {
String feature = channelId.toLowerCase();
if (productKey.equals(HIDDEN_DOOR_SENSOR_PRODUCT_KEY)) {
if (feature.equalsIgnoreCase(InsteonBindingConstants.BATTERY_LEVEL)
|| feature.equalsIgnoreCase(InsteonBindingConstants.BATTERY_WATERMARK_LEVEL)) {
feature = DATA;
}
} else if (productKey.equals(MOTION_SENSOR_PRODUCT_KEY)) {
if (feature.equalsIgnoreCase(InsteonBindingConstants.BATTERY_LEVEL)
|| feature.equalsIgnoreCase(InsteonBindingConstants.LIGHT_LEVEL)) {
feature = DATA;
}
} else if (productKey.equals(MOTION_SENSOR_II_PRODUCT_KEY)) {
if (feature.equalsIgnoreCase(InsteonBindingConstants.BATTERY_LEVEL)
|| feature.equalsIgnoreCase(InsteonBindingConstants.BATTERY_PERCENT)
|| feature.equalsIgnoreCase(InsteonBindingConstants.LIGHT_LEVEL)
|| feature.equalsIgnoreCase(InsteonBindingConstants.TEMPERATURE_LEVEL)) {
feature = DATA;
}
} else if (productKey.equals(PLM_PRODUCT_KEY)) {
String parts[] = feature.split("#");
if (parts.length == 2 && parts[0].equalsIgnoreCase(InsteonBindingConstants.BROADCAST_ON_OFF)
&& parts[1].matches("^\\d+$")) {
feature = BROADCAST_ON_OFF;
}
} else if (productKey.equals(POWER_METER_PRODUCT_KEY)) {
if (feature.equalsIgnoreCase(InsteonBindingConstants.KWH)
|| feature.equalsIgnoreCase(InsteonBindingConstants.RESET)
|| feature.equalsIgnoreCase(InsteonBindingConstants.UPDATE)
|| feature.equalsIgnoreCase(InsteonBindingConstants.WATTS)) {
feature = METER;
}
}
DeviceFeature f = device.getFeature(feature);
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 {
ChannelUID channelUID = new ChannelUID(thing.getUID(), channelId);
ChannelTypeUID channelTypeUID = new ChannelTypeUID(InsteonBindingConstants.BINDING_ID,
channelId);
Channel channel = thing.getChannel(channelUID);
if (channel == null) {
channel = getCallback().createChannelBuilder(channelUID, channelTypeUID).build();
}
addChannel(channel, channelId, channels, channelList);
}
} else {
logger.debug("{} is a feature group for {}. It will not be added as a channel.", feature,
productKey);
}
}
}
if (!channels.isEmpty() || device.isModem()) {
if (!channels.isEmpty()) {
updateThing(editThing().withChannels(channels).build());
}
StringBuilder builder = new StringBuilder(thingId);
builder.append(" address = ");
builder.append(address);
builder.append(" productKey = ");
builder.append(productKey);
builder.append(" channels = ");
builder.append(channelList.toString());
String msg = builder.toString();
logger.debug("{}", msg);
getInsteonNetworkHandler().initialized(getThing().getUID(), msg);
updateStatus(ThingStatus.ONLINE);
} else {
String msg = "Product key '" + productKey
+ "' does not have any features that match existing channels.";
logger.warn("{} {}", thing.getUID().getAsString(), msg);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, msg);
}
});
}
private void addChannel(Channel channel, String channelId, List<Channel> channels, StringBuilder channelList) {
channels.add(channel);
if (channelList.length() > 0) {
channelList.append(", ");
}
channelList.append(channelId);
}
@Override
public void dispose() {
String address = config.getAddress();
if (getBridge() != null && InsteonAddress.isValid(address)) {
getInsteonBinding().removeDevice(new InsteonAddress(address));
logger.debug("removed {} address = {}", getThing().getUID().getAsString(), address);
}
getInsteonNetworkHandler().disposed(getThing().getUID());
super.dispose();
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
logger.debug("channel {} was triggered with the command {}", channelUID.getAsString(), command);
getInsteonBinding().sendCommand(channelUID.getAsString(), command);
}
@Override
public void channelLinked(ChannelUID channelUID) {
Map<String, @Nullable String> params = new HashMap<>();
Channel channel = getThing().getChannel(channelUID.getId());
Map<String, Object> channelProperties = channel.getConfiguration().getProperties();
for (String key : channelProperties.keySet()) {
Object value = channelProperties.get(key);
if (value instanceof String) {
params.put(key, (String) value);
} else if (value instanceof BigDecimal) {
String s = ((BigDecimal) value).toPlainString();
params.put(key, s);
} else {
logger.warn("not a string or big decimal value key '{}' value '{}' {}", key, value,
value.getClass().getName());
}
}
String feature = channelUID.getId().toLowerCase();
String productKey = config.getProductKey();
if (productKey.equals(HIDDEN_DOOR_SENSOR_PRODUCT_KEY)) {
if (feature.equalsIgnoreCase(InsteonBindingConstants.BATTERY_LEVEL)) {
params.put(FIELD, FIELD_BATTERY_LEVEL);
feature = DATA;
} else if (feature.equalsIgnoreCase(InsteonBindingConstants.BATTERY_WATERMARK_LEVEL)) {
params.put(FIELD, FIELD_BATTERY_WATERMARK_LEVEL);
feature = DATA;
}
} else if (productKey.equals(MOTION_SENSOR_PRODUCT_KEY)) {
if (feature.equalsIgnoreCase(InsteonBindingConstants.BATTERY_LEVEL)) {
params.put(FIELD, FIELD_BATTERY_LEVEL);
feature = DATA;
} else if (feature.equalsIgnoreCase(InsteonBindingConstants.LIGHT_LEVEL)) {
params.put(FIELD, FIELD_LIGHT_LEVEL);
feature = DATA;
}
} else if (productKey.equals(MOTION_SENSOR_II_PRODUCT_KEY)) {
if (feature.equalsIgnoreCase(InsteonBindingConstants.BATTERY_LEVEL)) {
params.put(FIELD, FIELD_BATTERY_LEVEL);
feature = DATA;
} else if (feature.equalsIgnoreCase(InsteonBindingConstants.BATTERY_PERCENT)) {
params.put(FIELD, FIELD_BATTERY_PERCENTAGE);
feature = DATA;
} else if (feature.equalsIgnoreCase(InsteonBindingConstants.LIGHT_LEVEL)) {
params.put(FIELD, FIELD_LIGHT_LEVEL);
feature = DATA;
} else if (feature.equalsIgnoreCase(InsteonBindingConstants.TEMPERATURE_LEVEL)) {
params.put(FIELD, FIELD_TEMPERATURE_LEVEL);
feature = DATA;
}
} else if (productKey.equals(PLM_PRODUCT_KEY)) {
String parts[] = feature.split("#");
if (parts.length == 2 && parts[0].equalsIgnoreCase(InsteonBindingConstants.BROADCAST_ON_OFF)
&& parts[1].matches("^\\d+$")) {
params.put(GROUP, parts[1]);
feature = BROADCAST_ON_OFF;
}
} else if (productKey.equals(POWER_METER_PRODUCT_KEY)) {
if (feature.equalsIgnoreCase(InsteonBindingConstants.KWH)) {
params.put(FIELD, FIELD_KWH);
} else if (feature.equalsIgnoreCase(InsteonBindingConstants.WATTS)) {
params.put(FIELD, FIELD_WATTS);
} else if (feature.equalsIgnoreCase(InsteonBindingConstants.RESET)) {
params.put(CMD, CMD_RESET);
} else if (feature.equalsIgnoreCase(InsteonBindingConstants.UPDATE)) {
params.put(CMD, CMD_UPDATE);
}
feature = METER;
}
InsteonChannelConfiguration bindingConfig = new InsteonChannelConfiguration(channelUID, feature,
new InsteonAddress(config.getAddress()), productKey, params);
getInsteonBinding().addFeatureListener(bindingConfig);
StringBuilder builder = new StringBuilder(channelUID.getAsString());
builder.append(" feature = ");
builder.append(feature);
builder.append(" parameters = ");
builder.append(params);
String msg = builder.toString();
logger.debug("{}", msg);
getInsteonNetworkHandler().linked(channelUID, msg);
}
@Override
public void channelUnlinked(ChannelUID channelUID) {
getInsteonBinding().removeFeatureListener(channelUID);
getInsteonNetworkHandler().unlinked(channelUID);
logger.debug("channel {} unlinked ", channelUID.getAsString());
}
private @Nullable InsteonNetworkHandler getInsteonNetworkHandler() {
return (InsteonNetworkHandler) getBridge().getHandler();
}
private @Nullable InsteonBinding getInsteonBinding() {
return getInsteonNetworkHandler().getInsteonBinding();
}
}

View File

@@ -0,0 +1,219 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.insteon.internal.handler;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.insteon.internal.InsteonBinding;
import org.openhab.binding.insteon.internal.config.InsteonNetworkConfiguration;
import org.openhab.binding.insteon.internal.discovery.InsteonDeviceDiscoveryService;
import org.openhab.core.io.console.Console;
import org.openhab.core.io.transport.serial.SerialPortManager;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.thing.binding.BaseBridgeHandler;
import org.openhab.core.types.Command;
import org.openhab.core.types.State;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link InsteonNetworkHandler} is responsible for handling commands, which are
* sent to one of the channels.
*
* @author Rob Nielsen - Initial contribution
*/
@NonNullByDefault
@SuppressWarnings("null")
public class InsteonNetworkHandler extends BaseBridgeHandler {
private static final int LOG_DEVICE_STATISTICS_DELAY_IN_SECONDS = 600;
private static final int RETRY_DELAY_IN_SECONDS = 30;
private static final int SETTLE_TIME_IN_SECONDS = 5;
private final Logger logger = LoggerFactory.getLogger(InsteonNetworkHandler.class);
private @Nullable InsteonNetworkConfiguration config;
private @Nullable InsteonBinding insteonBinding;
private @Nullable InsteonDeviceDiscoveryService insteonDeviceDiscoveryService;
private @Nullable ScheduledFuture<?> pollingJob = null;
private @Nullable ScheduledFuture<?> reconnectJob = null;
private @Nullable ScheduledFuture<?> settleJob = null;
private long lastInsteonDeviceCreatedTimestamp = 0;
private @Nullable SerialPortManager serialPortManager;
private Map<String, String> deviceInfo = new ConcurrentHashMap<>();
private Map<String, String> channelInfo = new ConcurrentHashMap<>();
public static ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
public InsteonNetworkHandler(Bridge bridge, @Nullable SerialPortManager serialPortManager) {
super(bridge);
this.serialPortManager = serialPortManager;
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
}
@Override
public void initialize() {
logger.debug("Starting Insteon bridge");
config = getConfigAs(InsteonNetworkConfiguration.class);
updateStatus(ThingStatus.UNKNOWN);
scheduler.execute(() -> {
insteonBinding = new InsteonBinding(this, config, serialPortManager, scheduler);
// hold off on starting to poll until devices that already are defined as things are added.
// wait SETTLE_TIME_IN_SECONDS to start then check every second afterwards until it has been at
// least SETTLE_TIME_IN_SECONDS since last device was created.
settleJob = scheduler.scheduleWithFixedDelay(() -> {
// check to see if it has been at least SETTLE_TIME_IN_SECONDS since last device was created
if (System.currentTimeMillis() - lastInsteonDeviceCreatedTimestamp > SETTLE_TIME_IN_SECONDS * 1000) {
// settle time has expired start polling
if (insteonBinding.startPolling()) {
pollingJob = scheduler.scheduleWithFixedDelay(() -> {
insteonBinding.logDeviceStatistics();
}, 0, LOG_DEVICE_STATISTICS_DELAY_IN_SECONDS, TimeUnit.SECONDS);
insteonBinding.setIsActive(true);
updateStatus(ThingStatus.ONLINE);
} else {
String msg = "Initialization failed, unable to start the Insteon bridge with the port '"
+ config.getPort() + "'.";
logger.warn(msg);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, msg);
}
settleJob.cancel(false);
settleJob = null;
}
}, SETTLE_TIME_IN_SECONDS, 1, TimeUnit.SECONDS);
});
}
@Override
public void dispose() {
logger.debug("Shutting down Insteon bridge");
if (pollingJob != null) {
pollingJob.cancel(true);
pollingJob = null;
}
if (reconnectJob != null) {
reconnectJob.cancel(true);
reconnectJob = null;
}
if (settleJob != null) {
settleJob.cancel(true);
settleJob = null;
}
if (insteonBinding != null) {
insteonBinding.shutdown();
insteonBinding = null;
}
deviceInfo.clear();
channelInfo.clear();
super.dispose();
}
@Override
public void updateState(ChannelUID channelUID, State state) {
super.updateState(channelUID, state);
}
public void bindingDisconnected() {
reconnectJob = scheduler.scheduleWithFixedDelay(() -> {
if (insteonBinding.reconnect()) {
updateStatus(ThingStatus.ONLINE);
reconnectJob.cancel(false);
reconnectJob = null;
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Port disconnected.");
}
}, 0, RETRY_DELAY_IN_SECONDS, TimeUnit.SECONDS);
}
public void insteonDeviceWasCreated() {
lastInsteonDeviceCreatedTimestamp = System.currentTimeMillis();
}
public @Nullable InsteonBinding getInsteonBinding() {
return insteonBinding;
}
public void setInsteonDeviceDiscoveryService(InsteonDeviceDiscoveryService insteonDeviceDiscoveryService) {
this.insteonDeviceDiscoveryService = insteonDeviceDiscoveryService;
}
public void addMissingDevices(List<String> missing) {
scheduler.execute(() -> {
insteonDeviceDiscoveryService.addInsteonDevices(missing, getThing().getUID());
});
}
public void displayDevices(Console console) {
display(console, deviceInfo);
}
public void displayChannels(Console console) {
display(console, channelInfo);
}
public void displayLocalDatabase(Console console) {
Map<String, String> databaseInfo = insteonBinding.getDatabaseInfo();
console.println("local database contains " + databaseInfo.size() + " entries");
display(console, databaseInfo);
}
public void initialized(ThingUID uid, String msg) {
deviceInfo.put(uid.getAsString(), msg);
}
public void disposed(ThingUID uid) {
deviceInfo.remove(uid.getAsString());
}
public void linked(ChannelUID uid, String msg) {
channelInfo.put(uid.getAsString(), msg);
}
public void unlinked(ChannelUID uid) {
channelInfo.remove(uid.getAsString());
}
private void display(Console console, Map<String, String> info) {
ArrayList<String> ids = new ArrayList<>(info.keySet());
Collections.sort(ids);
for (String id : ids) {
console.println(info.get(id));
}
}
}

View File

@@ -0,0 +1,80 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.insteon.internal.message;
import java.util.HashMap;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* Defines the data types that can be used in the fields of a message.
*
* @author Daniel Pfrommer - Initial contribution
* @author Rob Nielsen - Port to openHAB 2 insteon binding
*/
@NonNullByDefault
public enum DataType {
BYTE("byte", 1),
INT("int", 4),
FLOAT("float", 4),
ADDRESS("address", 3),
INVALID("INVALID", -1);
private static HashMap<String, DataType> typeMap = new HashMap<>();
private int size = -1; // number of bytes consumed
private String name = "";
static {
typeMap.put(BYTE.getName(), BYTE);
typeMap.put(INT.getName(), INT);
typeMap.put(FLOAT.getName(), FLOAT);
typeMap.put(ADDRESS.getName(), ADDRESS);
}
/**
* Constructor
*
* @param name the name of the data type
* @param size the size (in bytes) of this data type
*/
DataType(String name, int size) {
this.size = size;
this.name = name;
}
/**
* @return the size (in bytes) of this data type
*/
public int getSize() {
return size;
}
/**
* @return clear text string with the name
*/
public String getName() {
return name;
}
/**
* Turns a string into the corresponding data type
*
* @param name the string to translate to a type
* @return the data type corresponding to the name string, or null if not found
*/
public static @Nullable DataType getDataType(String name) {
return typeMap.get(name);
}
}

View File

@@ -0,0 +1,215 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.insteon.internal.message;
import java.util.Objects;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.insteon.internal.device.InsteonAddress;
import org.openhab.binding.insteon.internal.utils.Utils;
/**
* An Insteon message has several fields with known type and offset
* within the message. This class represents a single field, and
* holds name, type, and offset (but not value!).
*
* @author Daniel Pfrommer - Initial contribution
* @author Rob Nielsen - Port to openHAB 2 insteon binding
*/
@NonNullByDefault
@SuppressWarnings("null")
public final class Field {
private final String name;
private final int offset;
private final @Nullable DataType type;
public String getName() {
return name;
}
public int getOffset() {
return offset;
}
public @Nullable DataType getType() {
return type;
}
public Field(String name, @Nullable DataType type, int off) {
this.name = name;
this.type = type;
this.offset = off;
}
private void check(int arrayLen, DataType t) throws FieldException {
checkSpace(arrayLen);
checkType(t);
}
private void checkSpace(int arrayLen) throws FieldException {
if (offset + type.getSize() > arrayLen) {
throw new FieldException("field write beyond end of msg");
}
}
private void checkType(DataType t) throws FieldException {
if (type != t) {
throw new FieldException("field write type mismatch!");
}
}
@Override
public String toString() {
return getName() + " Type: " + getType() + " Offset " + getOffset();
}
public String toString(byte @Nullable [] array) {
String s = name + ":";
try {
switch (type) {
case BYTE:
s += Utils.getHexByte(getByte(array));
break;
case INT:
s += Integer.toString(getInt(array));
break;
case ADDRESS:
s += getAddress(array).toString();
break;
default:
break;
}
} catch (FieldException e) {
// will just return empty string
}
return s;
}
public void set(byte @Nullable [] array, Object o) throws FieldException {
switch (getType()) {
case BYTE:
setByte(array, (Byte) o);
break;
case INT:
setInt(array, (Integer) o);
break;
// case FLOAT: setFloat(array, (float) o); break;
case ADDRESS:
setAddress(array, (InsteonAddress) o);
break;
default:
throw new FieldException("Not implemented data type " + getType() + "!");
}
}
/**
* Writes a byte value to a byte array, at the proper offset.
* Use this function to set the value of a field within a message.
*
* @param array the destination array
* @param b the value you want to set the byte to
* @throws FieldException
*/
public void setByte(byte @Nullable [] array, byte b) throws FieldException {
check(array.length, DataType.BYTE);
array[offset] = b;
}
/**
* Writes the value of an integer field to a byte array
* Use this function to set the value of a field within a message.
*
* @param array the destination array
* @param i the integer value to set
*/
public void setInt(byte @Nullable [] array, int i) throws FieldException {
check(array.length, DataType.INT);
array[offset] = (byte) ((i >>> 24) & 0xFF);
array[offset + 1] = (byte) ((i >>> 16) & 0xFF);
array[offset + 2] = (byte) ((i >>> 8) & 0xFF);
array[offset + 3] = (byte) ((i >>> 0) & 0xFF);
}
/**
* Writes the value of an InsteonAddress to a message array.
* Use this function to set the value of a field within a message.
*
* @param array the destination array
* @param adr the insteon address value to set
*/
public void setAddress(byte @Nullable [] array, InsteonAddress adr) throws FieldException {
check(array.length, DataType.ADDRESS);
adr.storeBytes(array, offset);
}
/**
* Fetch a byte from the array at the field position
*
* @param array the array to fetch from
* @return the byte value of the field
*/
public byte getByte(byte @Nullable [] array) throws FieldException {
check(array.length, DataType.BYTE);
return array[offset];
}
/**
* Fetch an int from the array at the field position
*
* @param array the array to fetch from
* @return the int value of the field
*/
public int getInt(byte @Nullable [] array) throws FieldException {
check(array.length, DataType.INT);
byte b1 = array[offset];
byte b2 = array[offset + 1];
byte b3 = array[offset + 2];
byte b4 = array[offset + 3];
int value = ((b1 << 24) + (b2 << 16) + (b3 << 8) + (b4 << 0));
return value;
}
/**
* Fetch an insteon address from the field position
*
* @param array the array to fetch from
* @return the address
*/
public InsteonAddress getAddress(byte @Nullable [] array) throws FieldException {
check(array.length, DataType.ADDRESS);
InsteonAddress adr = new InsteonAddress();
adr.loadBytes(array, offset);
return adr;
}
/**
* Equals test
*/
@Override
public boolean equals(@Nullable Object o) {
if (o instanceof Field) {
Field f = (Field) o;
return (f.getName().equals(getName())) && (f.getOffset() == getOffset());
} else {
return false;
}
}
@Override
public int hashCode() {
return Objects.hash(getName(), getOffset());
}
}

View File

@@ -0,0 +1,43 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.insteon.internal.message;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Exception to be thrown if there is an error processing a field, for
* example type mismatch, out of bounds etc.
*
* @author Daniel Pfrommer - Initial contribution
* @author Rob Nielsen - Port to openHAB 2 insteon binding
*/
@NonNullByDefault
public class FieldException extends Exception {
private static final long serialVersionUID = -4749311173073727318L;
public FieldException() {
super();
}
public FieldException(String m) {
super(m);
}
public FieldException(String m, Throwable cause) {
super(m, cause);
}
public FieldException(Throwable cause) {
super(cause);
}
}

View File

@@ -0,0 +1,29 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.insteon.internal.message;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Exception to be thrown from Msg class
*
* @author Rob Nielsen - Initial contribution
*/
@NonNullByDefault
public class InvalidMessageTypeException extends Exception {
private static final long serialVersionUID = -7582457696582413074L;
public InvalidMessageTypeException(String message) {
super(message);
}
}

View File

@@ -0,0 +1,619 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.insteon.internal.message;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Map;
import java.util.TreeSet;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.insteon.internal.device.InsteonAddress;
import org.openhab.binding.insteon.internal.utils.Utils;
import org.openhab.binding.insteon.internal.utils.Utils.ParsingException;
import org.osgi.framework.FrameworkUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Contains an Insteon Message consisting of the raw data, and the message definition.
* For more info, see the public Insteon Developer's Guide, 2nd edition,
* and the Insteon Modem Developer's Guide.
*
* @author Bernd Pfrommer - Initial contribution
* @author Daniel Pfrommer - openHAB 1 insteonplm binding
* @author Rob Nielsen - Port to openHAB 2 insteon binding
*/
@NonNullByDefault
@SuppressWarnings("null")
public class Msg {
private static final Logger logger = LoggerFactory.getLogger(Msg.class);
/**
* Represents the direction of the message from the host's view.
* The host is the machine to which the modem is attached.
*/
public enum Direction {
TO_MODEM("TO_MODEM"),
FROM_MODEM("FROM_MODEM");
private static HashMap<String, Direction> map = new HashMap<>();
private String directionString;
static {
map.put(TO_MODEM.getDirectionString(), TO_MODEM);
map.put(FROM_MODEM.getDirectionString(), FROM_MODEM);
}
Direction(String dirString) {
this.directionString = dirString;
}
public String getDirectionString() {
return directionString;
}
public static Direction getDirectionFromString(String dir) {
return map.get(dir);
}
}
// has the structure of all known messages
private static final Map<String, @Nullable Msg> MSG_MAP = new HashMap<>();
// maps between command number and the length of the header
private static final Map<Integer, @Nullable Integer> HEADER_MAP = new HashMap<>();
// has templates for all message from modem to host
private static final Map<Integer, @Nullable Msg> REPLY_MAP = new HashMap<>();
private int headerLength = -1;
private byte @Nullable [] data = null;
private MsgDefinition definition = new MsgDefinition();
private Direction direction = Direction.TO_MODEM;
private long quietTime = 0;
/**
* Constructor
*
* @param headerLength length of message header (in bytes)
* @param data byte array with message
* @param dataLength length of byte array data (in bytes)
* @param dir direction of the message (from/to modem)
*/
public Msg(int headerLength, byte[] data, int dataLength, Direction dir) {
this.headerLength = headerLength;
this.direction = dir;
initialize(data, 0, dataLength);
}
/**
* Copy constructor, needed to make a copy of the templates when
* generating messages from them.
*
* @param m the message to make a copy of
*/
public Msg(Msg m) {
headerLength = m.headerLength;
data = m.data.clone();
// the message definition usually doesn't change, but just to be sure...
definition = new MsgDefinition(m.definition);
direction = m.direction;
}
static {
// Use xml msg loader to load configs
try {
InputStream stream = FrameworkUtil.getBundle(Msg.class).getResource("/msg_definitions.xml").openStream();
if (stream != null) {
HashMap<String, Msg> msgs = XMLMessageReader.readMessageDefinitions(stream);
MSG_MAP.putAll(msgs);
} else {
logger.warn("could not get message definition resource!");
}
} catch (IOException e) {
logger.warn("i/o error parsing xml insteon message definitions", e);
} catch (ParsingException e) {
logger.warn("parse error parsing xml insteon message definitions", e);
} catch (FieldException e) {
logger.warn("got field exception while parsing xml insteon message definitions", e);
}
buildHeaderMap();
buildLengthMap();
}
//
// ------------------ simple getters and setters -----------------
//
/**
* Experience has shown that if Insteon messages are sent in close succession,
* only the first one will make it. The quiet time parameter says how long to
* wait after a message before the next one can be sent.
*
* @return the time (in milliseconds) to pause after message has been sent
*/
public long getQuietTime() {
return quietTime;
}
public byte @Nullable [] getData() {
return data;
}
public int getLength() {
return data.length;
}
public int getHeaderLength() {
return headerLength;
}
public Direction getDirection() {
return direction;
}
public MsgDefinition getDefinition() {
return definition;
}
public byte getCommandNumber() {
return ((data == null || data.length < 2) ? -1 : data[1]);
}
public boolean isPureNack() {
return (data.length == 2 && data[1] == 0x15);
}
public boolean isExtended() {
if (data == null || getLength() < 2) {
return false;
}
if (!definition.containsField("messageFlags")) {
return (false);
}
try {
byte flags = getByte("messageFlags");
return ((flags & 0x10) == 0x10);
} catch (FieldException e) {
// do nothing
}
return false;
}
public boolean isUnsolicited() {
// if the message has an ACK/NACK, it is in response to our message,
// otherwise it is out-of-band, i.e. unsolicited
return !definition.containsField("ACK/NACK");
}
public boolean isEcho() {
return isPureNack() || !isUnsolicited();
}
public boolean isOfType(MsgType mt) {
try {
MsgType t = MsgType.fromValue(getByte("messageFlags"));
return (t == mt);
} catch (FieldException e) {
return false;
}
}
public boolean isBroadcast() {
return isOfType(MsgType.ALL_LINK_BROADCAST) || isOfType(MsgType.BROADCAST);
}
public boolean isCleanup() {
return isOfType(MsgType.ALL_LINK_CLEANUP);
}
public boolean isAllLink() {
return isOfType(MsgType.ALL_LINK_BROADCAST) || isOfType(MsgType.ALL_LINK_CLEANUP);
}
public boolean isAckOfDirect() {
return isOfType(MsgType.ACK_OF_DIRECT);
}
public boolean isAllLinkCleanupAckOrNack() {
return isOfType(MsgType.ALL_LINK_CLEANUP_ACK) || isOfType(MsgType.ALL_LINK_CLEANUP_NACK);
}
public boolean isX10() {
try {
int cmd = getByte("Cmd") & 0xff;
if (cmd == 0x63 || cmd == 0x52) {
return true;
}
} catch (FieldException e) {
}
return false;
}
public void setDefinition(MsgDefinition d) {
definition = d;
}
public void setQuietTime(long t) {
quietTime = t;
}
public void addField(Field f) {
definition.addField(f);
}
public @Nullable InsteonAddress getAddr(String name) {
@Nullable
InsteonAddress a = null;
try {
a = definition.getField(name).getAddress(data);
} catch (FieldException e) {
// do nothing, we'll return null
}
return a;
}
public int getHopsLeft() throws FieldException {
int hops = (getByte("messageFlags") & 0x0c) >> 2;
return hops;
}
/**
* Will initialize the message with a byte[], an offset, and a length
*
* @param newData the src byte array
* @param offset the offset in the src byte array
* @param len the length to copy from the src byte array
*/
private void initialize(byte[] newData, int offset, int len) {
data = new byte[len];
if (offset >= 0 && offset < newData.length) {
System.arraycopy(newData, offset, data, 0, len);
} else {
logger.warn("intialize(): Offset out of bounds!");
}
}
/**
* Will put a byte at the specified key
*
* @param key the string key in the message definition
* @param value the byte to put
*/
public void setByte(@Nullable String key, byte value) throws FieldException {
Field f = definition.getField(key);
f.setByte(data, value);
}
/**
* Will put an int at the specified field key
*
* @param key the name of the field
* @param value the int to put
*/
public void setInt(String key, int value) throws FieldException {
Field f = definition.getField(key);
f.setInt(data, value);
}
/**
* Will put address bytes at the field
*
* @param key the name of the field
* @param adr the address to put
*/
public void setAddress(String key, InsteonAddress adr) throws FieldException {
Field f = definition.getField(key);
f.setAddress(data, adr);
}
/**
* Will fetch a byte
*
* @param key the name of the field
* @return the byte
*/
public byte getByte(String key) throws FieldException {
return (definition.getField(key).getByte(data));
}
/**
* Will fetch a byte array starting at a certain field
*
* @param key the name of the first field
* @param number of bytes to get
* @return the byte array
*/
public byte[] getBytes(String key, int numBytes) throws FieldException {
int offset = definition.getField(key).getOffset();
if (offset < 0 || offset + numBytes > data.length) {
throw new FieldException("data index out of bounds!");
}
byte[] section = new byte[numBytes];
System.arraycopy(data, offset, section, 0, numBytes);
return section;
}
/**
* Will fetch address from field
*
* @param field the filed name to fetch
* @return the address
*/
public InsteonAddress getAddress(String field) throws FieldException {
return (definition.getField(field).getAddress(data));
}
/**
* Fetch 3-byte (24bit) from message
*
* @param key1 the key of the msb
* @param key2 the key of the second msb
* @param key3 the key of the lsb
* @return the integer
*/
public int getInt24(String key1, String key2, String key3) throws FieldException {
int i = (definition.getField(key1).getByte(data) << 16) & (definition.getField(key2).getByte(data) << 8)
& definition.getField(key3).getByte(data);
return i;
}
public String toHexString() {
if (data != null) {
return Utils.getHexString(data);
}
return super.toString();
}
/**
* Sets the userData fields from a byte array
*
* @param data
*/
public void setUserData(byte[] arg) {
byte[] data = Arrays.copyOf(arg, 14); // appends zeros if short
try {
setByte("userData1", data[0]);
setByte("userData2", data[1]);
setByte("userData3", data[2]);
setByte("userData4", data[3]);
setByte("userData5", data[4]);
setByte("userData6", data[5]);
setByte("userData7", data[6]);
setByte("userData8", data[7]);
setByte("userData9", data[8]);
setByte("userData10", data[9]);
setByte("userData11", data[10]);
setByte("userData12", data[11]);
setByte("userData13", data[12]);
setByte("userData14", data[13]);
} catch (FieldException e) {
logger.warn("got field exception on msg {}:", e.getMessage());
}
}
/**
* Calculate and set the CRC with the older 1-byte method
*
* @return the calculated crc
*/
public int setCRC() {
int crc;
try {
crc = getByte("command1") + getByte("command2");
byte[] bytes = getBytes("userData1", 13); // skip userData14!
for (byte b : bytes) {
crc += b;
}
crc = ((~crc) + 1) & 0xFF;
setByte("userData14", (byte) (crc & 0xFF));
} catch (FieldException e) {
logger.warn("got field exception on msg {}:", this, e);
crc = 0;
}
return crc;
}
/**
* Calculate and set the CRC with the newer 2-byte method
*
* @return the calculated crc
*/
public int setCRC2() {
int crc = 0;
try {
byte[] bytes = getBytes("command1", 14);
for (int loop = 0; loop < bytes.length; loop++) {
int b = bytes[loop] & 0xFF;
for (int bit = 0; bit < 8; bit++) {
int fb = b & 0x01;
if ((crc & 0x8000) == 0) {
fb = fb ^ 0x01;
}
if ((crc & 0x4000) == 0) {
fb = fb ^ 0x01;
}
if ((crc & 0x1000) == 0) {
fb = fb ^ 0x01;
}
if ((crc & 0x0008) == 0) {
fb = fb ^ 0x01;
}
crc = ((crc << 1) | fb) & 0xFFFF;
b = b >> 1;
}
}
setByte("userData13", (byte) ((crc >> 8) & 0xFF));
setByte("userData14", (byte) (crc & 0xFF));
} catch (FieldException e) {
logger.warn("got field exception on msg {}:", this, e);
crc = 0;
}
return crc;
}
@Override
public String toString() {
String s = (direction == Direction.TO_MODEM) ? "OUT:" : "IN:";
if (data == null) {
return toHexString();
}
// need to first sort the fields by offset
Comparator<@Nullable Field> cmp = new Comparator<@Nullable Field>() {
@Override
public int compare(@Nullable Field f1, @Nullable Field f2) {
return f1.getOffset() - f2.getOffset();
}
};
TreeSet<@Nullable Field> fields = new TreeSet<>(cmp);
for (@Nullable
Field f : definition.getFields().values()) {
fields.add(f);
}
for (Field f : fields) {
if (f.getName().equals("messageFlags")) {
byte b;
try {
b = f.getByte(data);
MsgType t = MsgType.fromValue(b);
s += f.toString(data) + "=" + t.toString() + ":" + (b & 0x03) + ":" + ((b & 0x0c) >> 2) + "|";
} catch (FieldException e) {
logger.warn("toString error: ", e);
} catch (IllegalArgumentException e) {
logger.warn("toString msg type error: ", e);
}
} else {
s += f.toString(data) + "|";
}
}
return s;
}
/**
* Factory method to create Msg from raw byte stream received from the
* serial port.
*
* @param buf the raw received bytes
* @param msgLen length of received buffer
* @param isExtended whether it is an extended message or not
* @return message, or null if the Msg cannot be created
*/
public static @Nullable Msg createMessage(byte[] buf, int msgLen, boolean isExtended) {
if (buf == null || buf.length < 2) {
return null;
}
Msg template = REPLY_MAP.get(cmdToKey(buf[1], isExtended));
if (template == null) {
return null; // cannot find lookup map
}
if (msgLen != template.getLength()) {
logger.warn("expected msg {} len {}, got {}", template.getCommandNumber(), template.getLength(), msgLen);
return null;
}
Msg msg = new Msg(template.getHeaderLength(), buf, msgLen, Direction.FROM_MODEM);
msg.setDefinition(template.getDefinition());
return (msg);
}
/**
* Finds the header length from the insteon command in the received message
*
* @param cmd the insteon command received in the message
* @return the length of the header to expect
*/
public static int getHeaderLength(byte cmd) {
Integer len = HEADER_MAP.get((int) cmd);
if (len == null) {
return (-1); // not found
}
return len;
}
/**
* Tries to determine the length of a received Insteon message.
*
* @param b Insteon message command received
* @param isExtended flag indicating if it is an extended message
* @return message length, or -1 if length cannot be determined
*/
public static int getMessageLength(byte b, boolean isExtended) {
int key = cmdToKey(b, isExtended);
Msg msg = REPLY_MAP.get(key);
if (msg == null) {
return -1;
}
return msg.getLength();
}
/**
* From bytes received thus far, tries to determine if an Insteon
* message is extended or standard.
*
* @param buf the received bytes
* @param len the number of bytes received so far
* @param headerLength the known length of the header
* @return true if it is definitely extended, false if cannot be
* determined or if it is a standard message
*/
public static boolean isExtended(byte[] buf, int len, int headerLength) {
if (headerLength <= 2) {
return false;
} // extended messages are longer
if (len < headerLength) {
return false;
} // not enough data to tell if extended
byte flags = buf[headerLength - 1]; // last byte says flags
boolean isExtended = (flags & 0x10) == 0x10; // bit 4 is the message
return (isExtended);
}
/**
* Creates Insteon message (for sending) of a given type
*
* @param type the type of message to create, as defined in the xml file
* @return reference to message created
* @throws IOException if there is no such message type known
*/
public static Msg makeMessage(String type) throws InvalidMessageTypeException {
Msg m = MSG_MAP.get(type);
if (m == null) {
throw new InvalidMessageTypeException("unknown message type: " + type);
}
return new Msg(m);
}
private static int cmdToKey(byte cmd, boolean isExtended) {
return (cmd + (isExtended ? 256 : 0));
}
private static void buildHeaderMap() {
for (Msg m : MSG_MAP.values()) {
if (m.getDirection() == Direction.FROM_MODEM) {
HEADER_MAP.put((int) m.getCommandNumber(), m.getHeaderLength());
}
}
}
private static void buildLengthMap() {
for (Msg m : MSG_MAP.values()) {
if (m.getDirection() == Direction.FROM_MODEM) {
int key = cmdToKey(m.getCommandNumber(), m.isExtended());
REPLY_MAP.put(key, m);
}
}
}
}

View File

@@ -0,0 +1,72 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.insteon.internal.message;
import java.util.HashMap;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* Definition (layout) of an Insteon message. Says which bytes go where.
* For more info, see the public Insteon Developer's Guide, 2nd edition,
* and the Insteon Modem Developer's Guide.
*
* @author Daniel Pfrommer - Initial contribution
* @author Rob Nielsen - Port to openHAB 2 insteon binding
*/
@NonNullByDefault
@SuppressWarnings("null")
public class MsgDefinition {
private HashMap<String, @Nullable Field> fields = new HashMap<>();
MsgDefinition() {
}
/*
* Copy constructor, needed to make a copy of a message
*
* @param m the definition to copy
*/
MsgDefinition(@Nullable MsgDefinition m) {
fields = new HashMap<>(m.fields);
}
public HashMap<String, @Nullable Field> getFields() {
return fields;
}
public boolean containsField(String name) {
return fields.containsKey(name);
}
public void addField(Field field) {
fields.put(field.getName(), field);
}
/**
* Finds field of a given name
*
* @param name name of the field to search for
* @return reference to field
* @throws FieldException if no such field can be found
*/
public Field getField(@Nullable String name) throws FieldException {
@Nullable
Field f = fields.get(name);
if (f == null) {
throw new FieldException("field " + name + " not found");
}
return f;
}
}

View File

@@ -0,0 +1,170 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.insteon.internal.message;
import java.io.IOException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.insteon.internal.utils.Utils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This class takes data coming from the serial port and turns it
* into an message. For that, it has to figure out the length of the
* message from the header, and read enough bytes until it hits the
* message boundary. The code is tricky, partly because the Insteon protocol is.
* Most of the time the command code (second byte) is enough to determine the length
* of the incoming message, but sometimes one has to look deeper into the message
* to determine if it is a standard or extended message (their lengths differ).
*
* @author Bernd Pfrommer - Initial contribution
* @author Rob Nielsen - Port to openHAB 2 insteon binding
*/
@NonNullByDefault
public class MsgFactory {
private final Logger logger = LoggerFactory.getLogger(MsgFactory.class);
// no idea what the max msg length could be, but
// I doubt it'll ever be larger than 4k
private static final int MAX_MSG_LEN = 4096;
private byte[] buf = new byte[MAX_MSG_LEN];
private int end = 0; // offset of end of buffer
private boolean done = true; // done fully processing buffer flag
/**
* Constructor
*/
public MsgFactory() {
}
/**
* Indicates if no more complete message available in the buffer to be processed
*
* @return buffer data fully processed flag
*/
public boolean isDone() {
return done;
}
/**
* Adds incoming data to the data buffer. First call addData(), then call processData()
*
* @param data data to be added
* @param len length of data to be added
*/
public void addData(byte[] data, int len) {
int l = len;
if (l + end > MAX_MSG_LEN) {
logger.warn("truncating excessively long message!");
l = MAX_MSG_LEN - end;
}
// indicate new data can be processed if length > 0
if (l > 0) {
done = false;
}
// append the new data to the one we already have
System.arraycopy(data, 0, buf, end, l);
end += l;
// copy the incoming data to the end of the buffer
logger.trace("read buffer: len {} data: {}", end, Utils.getHexString(buf, end));
}
/**
* After data has been added, this method processes it.
* processData() needs to be called until it returns null, indicating that no
* more messages can be formed from the data buffer.
*
* @return a valid message, or null if the message is not complete
* @throws IOException if data was received with unknown command codes
*/
public @Nullable Msg processData() throws IOException {
Msg msg = null;
// handle the case where we get a pure nack
if (end > 0 && buf[0] == 0x15) {
logger.trace("got pure nack!");
removeFromBuffer(1);
try {
msg = Msg.makeMessage("PureNACK");
return msg;
} catch (InvalidMessageTypeException e) {
return null;
}
}
// drain the buffer until the first byte is 0x02
if (end > 0 && buf[0] != 0x02) {
bail("incoming message does not start with 0x02");
}
// Now see if we have enough data for a complete message.
// If not, we return null, and expect this method to be called again
// when more data has come in.
if (end > 1) {
// we have some data, but do we have enough to read the entire header?
int headerLength = Msg.getHeaderLength(buf[1]);
boolean isExtended = Msg.isExtended(buf, end, headerLength);
logger.trace("header length expected: {} extended: {}", headerLength, isExtended);
if (headerLength < 0) {
removeFromBuffer(1); // get rid of the leading 0x02 so draining works
bail("got unknown command code " + Utils.getHexByte(buf[0]));
} else if (headerLength >= 2) {
if (end >= headerLength) {
// only when the header is complete do we know that isExtended is correct!
int msgLen = Msg.getMessageLength(buf[1], isExtended);
logger.trace("msgLen expected: {}", msgLen);
if (msgLen < 0) {
// Cannot make sense out of the combined command code & isExtended flag.
removeFromBuffer(1);
bail("got unknown command code/ext flag " + Utils.getHexByte(buf[0]));
} else if (msgLen > 0) {
if (end >= msgLen) {
msg = Msg.createMessage(buf, msgLen, isExtended);
removeFromBuffer(msgLen);
}
} else { // should never happen
logger.warn("invalid message length, internal error!");
}
}
} else { // should never happen
logger.warn("invalid header length, internal error!");
}
}
// indicate no more messages available in buffer if empty or undefined message
if (end == 0 || msg == null) {
logger.trace("done processing current buffer data");
done = true;
}
logger.trace("keeping buffer len {} data: {}", end, Utils.getHexString(buf, end));
return msg;
}
private void bail(String txt) throws IOException {
drainBuffer(); // this will drain until end or it finds the next 0x02
logger.debug("bad data received: {}", txt);
throw new IOException(txt);
}
private void drainBuffer() {
while (end > 0 && buf[0] != 0x02) {
removeFromBuffer(1);
}
}
private void removeFromBuffer(int len) {
int l = len;
if (l > end) {
l = end;
}
System.arraycopy(buf, l, buf, 0, end + 1 - l);
end -= l;
}
}

View File

@@ -0,0 +1,31 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.insteon.internal.message;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Interface to receive Insteon messages from the modem.
*
* @author Bernd Pfrommer - Initial contribution
* @author Rob Nielsen - Port to openHAB 2 insteon binding
*/
@NonNullByDefault
public interface MsgListener {
/**
* Invoked whenever a valid message comes in from the modem
*
* @param msg the message received
*/
public abstract void msg(Msg msg);
}

View File

@@ -0,0 +1,83 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.insteon.internal.message;
import java.util.HashMap;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* Represents insteon message type flags
*
* @author Daniel Pfrommer - Initial contribution
* @author Rob Nielsen - Port to openHAB 2 insteon binding
*/
@NonNullByDefault
public enum MsgType {
/*
* From the official Insteon docs: the message flags are as follows:
*
* Bit 0 max hops low bit
* Bit 1 max hops high bit
* Bit 2 hops left low bit
* Bit 3 hops left high bit
* Bit 4 0: is standard message, 1: is extended message
* Bit 5 ACK
* Bit 6 0: not link related, 1: is ALL-Link message
* Bit 7 Broadcast/NAK
*/
BROADCAST(0x80),
DIRECT(0x00),
ACK_OF_DIRECT(0x20),
NACK_OF_DIRECT(0xa0),
ALL_LINK_BROADCAST(0xc0),
ALL_LINK_CLEANUP(0x40),
ALL_LINK_CLEANUP_ACK(0x60),
ALL_LINK_CLEANUP_NACK(0xe0),
INVALID(0xff); // should never happen
private static HashMap<Integer, @Nullable MsgType> hash = new HashMap<>();
private byte byteValue = 0;
/**
* Constructor
*
* @param b byte with insteon message type flags set
*/
MsgType(int b) {
this.byteValue = (byte) b;
}
static {
for (MsgType t : MsgType.values()) {
int i = t.getByteValue() & 0xff;
hash.put(i, t);
}
}
private int getByteValue() {
return byteValue;
}
public static MsgType fromValue(byte b) throws IllegalArgumentException {
int i = b & 0xe0;
@Nullable
MsgType mt = hash.get(i);
if (mt == null) {
throw new IllegalArgumentException("msg type of byte value " + i + " not found");
}
return mt;
}
}

View File

@@ -0,0 +1,175 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.insteon.internal.message;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map.Entry;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.insteon.internal.utils.Pair;
import org.openhab.binding.insteon.internal.utils.Utils.DataTypeParser;
import org.openhab.binding.insteon.internal.utils.Utils.ParsingException;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
/**
* Reads the Msg definitions from an XML file
*
* @author Daniel Pfrommer - Initial contribution
* @author Rob Nielsen - Port to openHAB 2 insteon binding
*/
@NonNullByDefault
@SuppressWarnings("null")
public class XMLMessageReader {
/**
* Reads the message definitions from an xml file
*
* @param input input stream from which to read
* @return what was read from file: the map between clear text string and Msg objects
* @throws IOException couldn't read file etc
* @throws ParsingException something wrong with the file format
* @throws FieldException something wrong with the field definition
*/
public static HashMap<String, Msg> readMessageDefinitions(InputStream input)
throws IOException, ParsingException, FieldException {
HashMap<String, Msg> messageMap = new HashMap<>();
try {
DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder dBuilder = dbFactory.newDocumentBuilder();
// Parse it!
Document doc = dBuilder.parse(input);
doc.getDocumentElement().normalize();
Node root = doc.getDocumentElement();
NodeList nodes = root.getChildNodes();
for (int i = 0; i < nodes.getLength(); i++) {
Node node = nodes.item(i);
if (node.getNodeType() == Node.ELEMENT_NODE) {
if (node.getNodeName().equals("msg")) {
Pair<String, Msg> msgDef = readMessageDefinition((Element) node);
messageMap.put(msgDef.getKey(), msgDef.getValue());
}
}
}
} catch (SAXException e) {
throw new ParsingException("Failed to parse XML!", e);
} catch (ParserConfigurationException e) {
throw new ParsingException("Got parser config exception! ", e);
}
return messageMap;
}
private static Pair<String, Msg> readMessageDefinition(Element msg) throws FieldException, ParsingException {
int length = 0;
int hlength = 0;
LinkedHashMap<Field, Object> fieldMap = new LinkedHashMap<>();
String dir = msg.getAttribute("direction");
String name = msg.getAttribute("name");
Msg.Direction direction = Msg.Direction.getDirectionFromString(dir);
if (msg.hasAttribute("length")) {
length = Integer.parseInt(msg.getAttribute("length"));
}
NodeList nodes = msg.getChildNodes();
int offset = 0;
for (int i = 0; i < nodes.getLength(); i++) {
Node node = nodes.item(i);
if (node.getNodeType() == Node.ELEMENT_NODE) {
if (node.getNodeName().equals("header")) {
int o = readHeaderElement((Element) node, fieldMap);
hlength = o;
// Increment the offset by the header length
offset += o;
} else {
Pair<Field, Object> field = readField((Element) node, offset);
fieldMap.put(field.getKey(), field.getValue());
// Increment the offset
offset += field.getKey().getType().getSize();
}
}
}
if (offset != length) {
throw new ParsingException(
"Actual msg length " + offset + " differs from given msg length " + length + "!");
}
if (length == 0) {
length = offset;
}
return new Pair<>(name, createMsg(fieldMap, length, hlength, direction));
}
private static int readHeaderElement(Element header, LinkedHashMap<Field, Object> fields) throws ParsingException {
int offset = 0;
int headerLen = Integer.parseInt(header.getAttribute("length"));
NodeList nodes = header.getChildNodes();
for (int i = 0; i < nodes.getLength(); i++) {
Node node = nodes.item(i);
if (node.getNodeType() == Node.ELEMENT_NODE) {
@Nullable
Pair<Field, Object> definition = readField((Element) node, offset);
if (definition != null) {
offset += definition.getKey().getType().getSize();
fields.put(definition.getKey(), definition.getValue());
}
}
}
if (headerLen != offset) {
throw new ParsingException(
"Actual header length " + offset + " differs from given length " + headerLen + "!");
}
return headerLen;
}
private static Pair<Field, Object> readField(Element field, int offset) {
DataType dType = DataType.getDataType(field.getTagName());
// Will return blank if no name attribute
String name = field.getAttribute("name");
Field f = new Field(name, dType, offset);
// Now we have field, only need value
String sVal = field.getTextContent();
Object val = DataTypeParser.parseDataType(dType, sVal);
Pair<Field, Object> pair = new Pair<>(f, val);
return pair;
}
private static Msg createMsg(HashMap<Field, Object> values, int length, int headerLength, Msg.Direction dir)
throws FieldException {
Msg msg = new Msg(headerLength, new byte[length], length, dir);
for (Entry<Field, Object> e : values.entrySet()) {
Field f = e.getKey();
f.set(msg.getData(), e.getValue());
if (f.getName() != null && !f.getName().equals("")) {
msg.addField(f);
}
}
return msg;
}
}

View File

@@ -0,0 +1,46 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.insteon.internal.utils;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Generic pair class.
*
* @author Daniel Pfrommer - Initial contribution
* @author Rob Nielsen - Port to openHAB 2 insteon binding
*/
@NonNullByDefault
public class Pair<K, V> {
private K key;
private V value;
/**
* Constructs a new <code>Pair</code> with a given key/value
*
* @param key the key
* @param value the value
*/
public Pair(K key, V value) {
this.key = key;
this.value = value;
}
public K getKey() {
return key;
}
public V getValue() {
return value;
}
}

View File

@@ -0,0 +1,142 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.insteon.internal.utils;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.insteon.internal.device.InsteonAddress;
import org.openhab.binding.insteon.internal.message.DataType;
/**
* Various utility functions for e.g. hex string parsing
*
* @author Daniel Pfrommer - Initial contribution
* @author Rob Nielsen - Port to openHAB 2 insteon binding
*/
@NonNullByDefault
@SuppressWarnings("null")
public class Utils {
public static String getHexString(int b) {
String result = String.format("%02X", b & 0xFF);
return result;
}
public static String getHexString(byte @Nullable [] b) {
return getHexString(b, b.length);
}
public static String getHexString(byte[] b, int len) {
String result = "";
for (int i = 0; i < b.length && i < len; i++) {
result += String.format("%02X ", b[i] & 0xFF);
}
return result;
}
public static int strToInt(String s) throws NumberFormatException {
int ret = -1;
if (s.startsWith("0x")) {
ret = Integer.parseInt(s.substring(2), 16);
} else {
ret = Integer.parseInt(s);
}
return (ret);
}
public static int fromHexString(String string) {
return Integer.parseInt(string, 16);
}
public static int from0xHexString(String string) {
String hex = string.substring(2);
return fromHexString(hex);
}
public static String getHexByte(byte b) {
return String.format("0x%02X", b & 0xFF);
}
public static String getHexByte(int b) {
return String.format("0x%02X", b);
}
@NonNullByDefault
public static class DataTypeParser {
public static Object parseDataType(@Nullable DataType type, String val) {
switch (type) {
case BYTE:
return parseByte(val);
case INT:
return parseInt(val);
case FLOAT:
return parseFloat(val);
case ADDRESS:
return parseAddress(val);
default:
throw new IllegalArgumentException("Data Type not implemented in Field Value Parser!");
}
}
public static byte parseByte(@Nullable String val) {
if (val != null && !val.trim().equals("")) {
return (byte) Utils.from0xHexString(val.trim());
} else {
return 0x00;
}
}
public static int parseInt(@Nullable String val) {
if (val != null && !val.trim().equals("")) {
return Integer.parseInt(val);
} else {
return 0x00;
}
}
public static float parseFloat(@Nullable String val) {
if (val != null && !val.trim().equals("")) {
return Float.parseFloat(val.trim());
} else {
return 0;
}
}
public static InsteonAddress parseAddress(@Nullable String val) {
if (val != null && !val.trim().equals("")) {
return InsteonAddress.parseAddress(val.trim());
} else {
return new InsteonAddress();
}
}
}
/**
* Exception to indicate various xml parsing errors.
*/
@NonNullByDefault
public static class ParsingException extends Exception {
private static final long serialVersionUID = 3997461423241843949L;
public ParsingException(String msg) {
super(msg);
}
public ParsingException(String msg, Throwable cause) {
super(msg, cause);
}
}
public static String redactPassword(String port) {
return !port.startsWith("/hub2/") ? port : port.replaceAll(":\\w+@", ":******@");
}
}

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<binding:binding id="insteon" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:binding="https://openhab.org/schemas/binding/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/binding/v1.0.0 https://openhab.org/schemas/binding-1.0.0.xsd">
<name>Insteon Binding</name>
<description>This is the binding for Insteon.</description>
<author>Rob Nielsen</author>
</binding:binding>

View File

@@ -0,0 +1,503 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="insteon"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<bridge-type id="network">
<label>Insteon Network</label>
<description>An Insteon PLM or hub that is used to communicate with the Insteon devices.</description>
<config-description>
<parameter name="port" type="text" required="true">
<label>Port</label>
<description>Configuration information that is used to connect to PLM or hub.</description>
</parameter>
<parameter name="devicePollIntervalSeconds" type="integer" min="5" max="3600">
<label>Device Poll Interval</label>
<description>Device poll interval in seconds.</description>
</parameter>
<parameter name="additionalDevices" type="text">
<label>Additional Devices</label>
<description>Optional file with additional device types.</description>
</parameter>
<parameter name="additionalFeatures" type="text">
<label>Additional Features</label>
<description>Optional file with additional feature templates.</description>
</parameter>
</config-description>
</bridge-type>
<thing-type id="device">
<supported-bridge-type-refs>
<bridge-type-ref id="network"/>
</supported-bridge-type-refs>
<label>Insteon Device</label>
<description>Insteon devices such as switches, dimmers, keypads, sensors, etc.</description>
<config-description>
<parameter name="address" type="text" required="true">
<label>Address</label>
<description>Insteon address of the device. Example: 12.34.56</description>
</parameter>
<parameter name="productKey" type="text" required="true">
<label>Product Key</label>
<description>Insteon binding product key that is used to identify the model of the device.</description>
<options>
<option value="F00.00.01">2477D SwitchLinc Dimmer - F00.00.01</option>
<option value="F00.00.02">2477S SwitchLinc Switch - F00.00.02</option>
<option value="F00.00.03">2845-222 Hidden Door Sensor - F00.00.03</option>
<option value="F00.00.04">2876S ICON Switch - F00.00.04</option>
<option value="F00.00.05">2456D3 LampLinc V2 - F00.00.05</option>
<option value="F00.00.06">2442-222 Micro Dimmer - F00.00.06</option>
<option value="F00.00.07">2453-222 DIN Rail On/Off - F00.00.07</option>
<option value="F00.00.08">2452-222 DIN Rail Dimmer - F00.00.08</option>
<option value="F00.00.09">2458-A1 MorningLinc RF Lock Controller - F00.00.09</option>
<option value="F00.00.0A">2852-222 Leak Sensor - F00.00.0A</option>
<option value="F00.00.0B">2672-422 LED Dimmer - F00.00.0B</option>
<option value="F00.00.0C">2476D SwitchLinc Dimmer - F00.00.0C</option>
<option value="F00.00.0D">2634-222 On/Off Dual-Band Outdoor Module - F00.00.0D</option>
<option value="F00.00.10">2342-2 Mini Remote - F00.00.10</option>
<option value="F00.00.11">2466D ToggleLinc Dimmer - F00.00.11</option>
<option value="F00.00.12">2466S ToggleLinc Switch - F00.00.12</option>
<option value="F00.00.13">2672-222 LED Bulb - F00.00.13</option>
<option value="F00.00.14">2487S KeypadLinc On/Off 6-Button - F00.00.14</option>
<option value="F00.00.15">2334-232 KeypadLink Dimmer 6-Button - F00.00.15</option>
<option value="F00.00.16">2334-232 KeypadLink Dimmer 8-Button - F00.00.16</option>
<option value="F00.00.17">2423A1 iMeter Solo Power Meter - F00.00.17</option>
<option value="F00.00.18">2423A1 Thermostat 2441TH - F00.00.18</option>
<option value="F00.00.19">2457D2 LampLinc Dimmer - F00.00.19</option>
<option value="F00.00.1A">2475SDB In-LineLinc Relay - F00.00.1A</option>
<option value="F00.00.1B">2635-222 On/Off Module - F00.00.1B</option>
<option value="F00.00.1C">2475F FanLinc Module - F00.00.1C</option>
<option value="F00.00.1D">2456S3 ApplianceLinc - F00.00.1D</option>
<option value="F00.00.1E">2674-222 LED Bulb (Recessed) - F00.00.1E</option>
<option value="F00.00.1F">2477SA1 220V 30-amp Load Controller N/O - F00.00.1F</option>
<option value="F00.00.20">2342-222 Mini Remote (8-Button) - F00.00.20</option>
<option value="F00.00.21">2441V Insteon Thermostat Adaptor for Venstar - F00.00.21</option>
<option value="F00.00.22">2982-222 Insteon Smoke Bridge - F00.00.22</option>
<option value="F00.00.23">2487S KeypadLinc On/Off 8-Button - F00.00.23</option>
<option value="F00.00.24">Motion Sensor II - F00.00.24</option>
<option value="0x00001A">2450 IO Link - 0x00001A</option>
<option value="0x000037">2486D KeypadLinc Dimmer - 0x000037</option>
<option value="0x000039">2663-222 On/Off Outlet - 0x000039</option>
<option value="0x000041">2484DWH8 KeypadLinc Countdown Timer - 0x000041</option>
<option value="0x000045">PLM or hub - 0x000045</option>
<option value="0x000049">2843-222 Wireless Open/Close Sensor - 0x000049</option>
<option value="0x00004A">2842-222 Motion Sensor - 0x00004A</option>
<option value="0x000051">2486DWH8 KeypadLinc Dimmer - 0x000051</option>
<option value="0x000068">2472D OutletLinc Dimmer - 0x000068</option>
<option value="X00.00.01">X10 switch Generic X10 switch - X00.00.01</option>
<option value="X00.00.02">X10 dimmer Generic X10 dimmer - X00.00.02</option>
<option value="X00.00.03">X10 motion Generic X10 motion sensor - X00.00.03</option>
</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>
<channel-type id="acDelay">
<item-type>Number</item-type>
<label>AC Delay</label>
</channel-type>
<channel-type id="backlightDuration">
<item-type>Number</item-type>
<label>Back Light Duration</label>
</channel-type>
<channel-type id="batteryLevel">
<item-type>Number</item-type>
<label>Battery Level</label>
</channel-type>
<channel-type id="batteryPercent">
<item-type>Number:Dimensionless</item-type>
<label>Battery Percent</label>
</channel-type>
<channel-type id="batteryWatermarkLevel">
<item-type>Number</item-type>
<label>Battery Watermark Level</label>
</channel-type>
<channel-type id="beep">
<item-type>Switch</item-type>
<label>Beep</label>
</channel-type>
<channel-type id="bottomOutlet">
<item-type>Switch</item-type>
<label>Bottom Outlet</label>
</channel-type>
<channel-type id="buttonA">
<item-type>Switch</item-type>
<label>Button A</label>
</channel-type>
<channel-type id="buttonB">
<item-type>Switch</item-type>
<label>Button B</label>
</channel-type>
<channel-type id="buttonC">
<item-type>Switch</item-type>
<label>Button C</label>
</channel-type>
<channel-type id="buttonD">
<item-type>Switch</item-type>
<label>Button D</label>
</channel-type>
<channel-type id="buttonE">
<item-type>Switch</item-type>
<label>Button E</label>
</channel-type>
<channel-type id="buttonF">
<item-type>Switch</item-type>
<label>Button F</label>
</channel-type>
<channel-type id="buttonG">
<item-type>Switch</item-type>
<label>Button G</label>
</channel-type>
<channel-type id="buttonH">
<item-type>Switch</item-type>
<label>Button H</label>
</channel-type>
<channel-type id="broadcastOnOff">
<item-type>Switch</item-type>
<label>Broadcast On/Off</label>
</channel-type>
<channel-type id="contact">
<item-type>Contact</item-type>
<label>Contact</label>
</channel-type>
<channel-type id="coolSetPoint">
<item-type>Number</item-type>
<label>Cool Set Point</label>
</channel-type>
<channel-type id="dimmer">
<item-type>Dimmer</item-type>
<label>Dimmer</label>
</channel-type>
<channel-type id="fan">
<item-type>Number</item-type>
<label>Fan</label>
</channel-type>
<channel-type id="fanMode">
<item-type>Number</item-type>
<label>Fan Mode</label>
</channel-type>
<channel-type id="fastOnOff">
<item-type>Switch</item-type>
<label>Fast On/Off</label>
</channel-type>
<channel-type id="fastOnOffButtonA">
<item-type>Switch</item-type>
<label>Fast On/Off Button A</label>
</channel-type>
<channel-type id="fastOnOffButtonB">
<item-type>Switch</item-type>
<label>Fast On/Off Button B</label>
</channel-type>
<channel-type id="fastOnOffButtonC">
<item-type>Switch</item-type>
<label>Fast On/Off Button C</label>
</channel-type>
<channel-type id="fastOnOffButtonD">
<item-type>Switch</item-type>
<label>Fast On/Off Button D</label>
</channel-type>
<channel-type id="fastOnOffButtonE">
<item-type>Switch</item-type>
<label>Fast On/Off Button E</label>
</channel-type>
<channel-type id="fastOnOffButtonF">
<item-type>Switch</item-type>
<label>Fast On/Off Button F</label>
</channel-type>
<channel-type id="fastOnOffButtonG">
<item-type>Switch</item-type>
<label>Fast On/Off Button G</label>
</channel-type>
<channel-type id="fastOnOffButtonH">
<item-type>Switch</item-type>
<label>Fast On/Off Button H</label>
</channel-type>
<channel-type id="heatSetPoint">
<item-type>Number</item-type>
<label>Heat Set Point</label>
</channel-type>
<channel-type id="humidity">
<item-type>Number</item-type>
<label>Humidity</label>
</channel-type>
<channel-type id="humidityHigh">
<item-type>Number</item-type>
<label>Humidity High</label>
</channel-type>
<channel-type id="humidityLow">
<item-type>Number</item-type>
<label>Humidity Low</label>
</channel-type>
<channel-type id="isCooling">
<item-type>Number</item-type>
<label>Is Cooling</label>
</channel-type>
<channel-type id="isHeating">
<item-type>Number</item-type>
<label>Is Heating</label>
</channel-type>
<channel-type id="keypadButtonA">
<item-type>Switch</item-type>
<label>Keypad Button A</label>
</channel-type>
<channel-type id="keypadButtonB">
<item-type>Switch</item-type>
<label>Keypad Button B</label>
</channel-type>
<channel-type id="keypadButtonC">
<item-type>Switch</item-type>
<label>Keypad Button C</label>
</channel-type>
<channel-type id="keypadButtonD">
<item-type>Switch</item-type>
<label>Keypad Button D</label>
</channel-type>
<channel-type id="keypadButtonE">
<item-type>Switch</item-type>
<label>Keypad Button E</label>
</channel-type>
<channel-type id="keypadButtonF">
<item-type>Switch</item-type>
<label>Keypad Button F</label>
</channel-type>
<channel-type id="keypadButtonG">
<item-type>Switch</item-type>
<label>Keypad Button G</label>
</channel-type>
<channel-type id="keypadButtonH">
<item-type>Switch</item-type>
<label>Keypad Button H</label>
</channel-type>
<channel-type id="kWh">
<item-type>Number:Energy</item-type>
<label>Kilowatt Hour</label>
</channel-type>
<channel-type id="lastHeardFrom">
<item-type>DateTime</item-type>
<label>Last Heard From</label>
</channel-type>
<channel-type id="ledBrightness">
<item-type>Number</item-type>
<label>LED Brightness</label>
</channel-type>
<channel-type id="ledOnOff">
<item-type>Switch</item-type>
<label>LED On/Off</label>
</channel-type>
<channel-type id="lightDimmer">
<item-type>Dimmer</item-type>
<label>Light Dimmer</label>
</channel-type>
<channel-type id="lightLevel">
<item-type>Number</item-type>
<label>Light Level</label>
</channel-type>
<channel-type id="lightLevelAboveThreshold">
<item-type>Contact</item-type>
<label>Light Level Above/Below Threshold</label>
</channel-type>
<channel-type id="loadDimmer">
<item-type>Dimmer</item-type>
<label>Load Dimmer</label>
</channel-type>
<channel-type id="loadSwitch">
<item-type>Switch</item-type>
<label>Load Switch</label>
</channel-type>
<channel-type id="loadSwitchFastOnOff">
<item-type>Switch</item-type>
<label>Load Switch Fast On/Off</label>
</channel-type>
<channel-type id="loadSwitchManualChange">
<item-type>Number</item-type>
<label>Load Switch Manual Change</label>
</channel-type>
<channel-type id="lowBattery">
<item-type>Contact</item-type>
<label>Low Battery</label>
</channel-type>
<channel-type id="manualChange">
<item-type>Number</item-type>
<label>Manual Change</label>
</channel-type>
<channel-type id="manualChangeButtonA">
<item-type>Number</item-type>
<label>Manual Change Button A</label>
</channel-type>
<channel-type id="manualChangeButtonB">
<item-type>Number</item-type>
<label>Manual Change Button B</label>
</channel-type>
<channel-type id="manualChangeButtonC">
<item-type>Number</item-type>
<label>Manual Change Button C</label>
</channel-type>
<channel-type id="manualChangeButtonD">
<item-type>Number</item-type>
<label>Manual Change Button D</label>
</channel-type>
<channel-type id="manualChangeButtonE">
<item-type>Number</item-type>
<label>Manual Change Button E</label>
</channel-type>
<channel-type id="manualChangeButtonF">
<item-type>Number</item-type>
<label>Manual Change Button F</label>
</channel-type>
<channel-type id="manualChangeButtonG">
<item-type>Number</item-type>
<label>Manual Change Button G</label>
</channel-type>
<channel-type id="manualChangeButtonH">
<item-type>Number</item-type>
<label>Manual Change Button H</label>
</channel-type>
<channel-type id="notification">
<item-type>Number</item-type>
<label>Notification</label>
</channel-type>
<channel-type id="onLevel">
<item-type>Number</item-type>
<label>On Level</label>
</channel-type>
<channel-type id="rampDimmer">
<item-type>Dimmer</item-type>
<label>Ramp Dimmer</label>
</channel-type>
<channel-type id="rampRate">
<item-type>Number</item-type>
<label>Ramp Rate</label>
</channel-type>
<channel-type id="reset">
<item-type>Switch</item-type>
<label>Reset</label>
</channel-type>
<channel-type id="stage1Duration">
<item-type>Number</item-type>
<label>Stage 1 Duration</label>
</channel-type>
<channel-type id="switch">
<item-type>Switch</item-type>
<label>Switch</label>
</channel-type>
<channel-type id="systemMode">
<item-type>Number</item-type>
<label>System Mode</label>
</channel-type>
<channel-type id="tamperSwitch">
<item-type>Contact</item-type>
<label>Tamper Switch</label>
</channel-type>
<channel-type id="temperature">
<item-type>Number:Temperature</item-type>
<label>Temperature</label>
</channel-type>
<channel-type id="temperatureLevel">
<item-type>Number</item-type>
<label>Temperature Level</label>
</channel-type>
<channel-type id="topOutlet">
<item-type>Switch</item-type>
<label>Top Outlet</label>
</channel-type>
<channel-type id="update">
<item-type>Switch</item-type>
<label>Update</label>
</channel-type>
<channel-type id="watts">
<item-type>Number:Power</item-type>
<label>Watts</label>
</channel-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,897 @@
<xml>
<feature name="GenericSwitch" timeout="5000">
<message-dispatcher>DefaultDispatcher</message-dispatcher>
<message-handler cmd="0x03" group="1">NoOpMsgHandler</message-handler>
<message-handler cmd="0x11" group="1">LightOnSwitchHandler</message-handler>
<message-handler cmd="0x12" group="1" mode="FAST">LightOnSwitchHandler</message-handler>
<message-handler cmd="0x13" group="1">LightOffSwitchHandler</message-handler>
<message-handler cmd="0x14" group="1" mode="FAST">LightOffSwitchHandler</message-handler>
<message-handler cmd="0x19">SwitchRequestReplyHandler</message-handler>
<command-handler command="OnOffType">LightOnOffCommandHandler</command-handler>
<poll-handler ext="0" cmd1="0x19" cmd2="0x00">FlexPollHandler</poll-handler>
</feature>
<feature name="FastOnOff">
<message-dispatcher>DefaultDispatcher</message-dispatcher>
<message-handler cmd="0x12" mode="FAST">LightOnSwitchHandler</message-handler>
<message-handler cmd="0x14" mode="FAST">LightOffSwitchHandler</message-handler>
<command-handler command="OnOffType">FastOnOffCommandHandler</command-handler>
<poll-handler>NoPollHandler</poll-handler>
</feature>
<feature name="RampDimmer">
<message-dispatcher>DefaultDispatcher</message-dispatcher>
<!-- cmd1 defaults to 0x2E, 0x2F -->
<!-- default dispatcher uses 0x19 for lookup key instead of cmd1 -->
<message-handler cmd="0x19">RampDimmerHandler</message-handler>
<command-handler command="PercentType">RampPercentHandler</command-handler>
<command-handler command="OnOffType">RampOnOffCommandHandler</command-handler>
</feature>
<feature name="RampDimmer_3435">
<message-dispatcher>DefaultDispatcher</message-dispatcher>
<!-- default dispatcher uses 0x19 for lookup key instead of cmd1 -->
<message-handler cmd="0x19" on="0x34" off="0x35">RampDimmerHandler</message-handler>
<command-handler command="PercentType" on="0x34" off="0x35">RampPercentHandler</command-handler>
<command-handler command="OnOffType" on="0x34" off="0x35">RampOnOffCommandHandler</command-handler>
</feature>
<feature name="ManualChange">
<message-dispatcher>DefaultDispatcher</message-dispatcher>
<message-handler cmd="0x17">StartManualChangeHandler</message-handler>
<message-handler cmd="0x18">StopManualChangeHandler</message-handler>
<command-handler command="DecimalType">ManualChangeCommandHandler</command-handler>
<poll-handler>NoPollHandler</poll-handler>
</feature>
<feature name="RemoteButton1">
<message-dispatcher>DefaultDispatcher</message-dispatcher>
<message-handler cmd="0x03" group="1">NoOpMsgHandler</message-handler>
<message-handler cmd="0x11" button="1" group="1">LightOnSwitchHandler</message-handler>
<message-handler cmd="0x13" button="1" group="1">LightOffSwitchHandler</message-handler>
<poll-handler>NoPollHandler</poll-handler>
</feature>
<feature name="RemoteButton2">
<message-dispatcher>DefaultDispatcher</message-dispatcher>
<message-handler cmd="0x03" group="2">NoOpMsgHandler</message-handler>
<message-handler cmd="0x11" button="2" group="2">LightOnSwitchHandler</message-handler>
<message-handler cmd="0x13" button="2" group="2">LightOffSwitchHandler</message-handler>
<poll-handler>NoPollHandler</poll-handler>
</feature>
<feature name="RemoteButton3">
<message-dispatcher>DefaultDispatcher</message-dispatcher>
<message-handler cmd="0x03" group="3">NoOpMsgHandler</message-handler>
<message-handler cmd="0x11" button="3" group="3">LightOnSwitchHandler</message-handler>
<message-handler cmd="0x13" button="3" group="3">LightOffSwitchHandler</message-handler>
<poll-handler>NoPollHandler</poll-handler>
</feature>
<feature name="RemoteButton4">
<message-dispatcher>DefaultDispatcher</message-dispatcher>
<message-handler cmd="0x03" group="4">NoOpMsgHandler</message-handler>
<message-handler cmd="0x11" button="4" group="4">LightOnSwitchHandler</message-handler>
<message-handler cmd="0x13" button="4" group="4">LightOffSwitchHandler</message-handler>
<poll-handler>NoPollHandler</poll-handler>
</feature>
<feature name="RemoteButton5">
<message-dispatcher>DefaultDispatcher</message-dispatcher>
<message-handler cmd="0x03" group="5">NoOpMsgHandler</message-handler>
<message-handler cmd="0x11" button="5" group="5">LightOnSwitchHandler</message-handler>
<message-handler cmd="0x13" button="5" group="5">LightOffSwitchHandler</message-handler>
<poll-handler>NoPollHandler</poll-handler>
</feature>
<feature name="RemoteButton6">
<message-dispatcher>DefaultDispatcher</message-dispatcher>
<message-handler cmd="0x03" group="6">NoOpMsgHandler</message-handler>
<message-handler cmd="0x11" button="6" group="6">LightOnSwitchHandler</message-handler>
<message-handler cmd="0x13" button="6" group="6">LightOffSwitchHandler</message-handler>
<poll-handler>NoPollHandler</poll-handler>
</feature>
<feature name="RemoteButton7">
<message-dispatcher>DefaultDispatcher</message-dispatcher>
<message-handler cmd="0x03" group="7">NoOpMsgHandler</message-handler>
<message-handler cmd="0x11" button="7" group="7">LightOnSwitchHandler</message-handler>
<message-handler cmd="0x13" button="7" group="7">LightOffSwitchHandler</message-handler>
<poll-handler>NoPollHandler</poll-handler>
</feature>
<feature name="RemoteButton8">
<message-dispatcher>DefaultDispatcher</message-dispatcher>
<message-handler cmd="0x03" group="8">NoOpMsgHandler</message-handler>
<message-handler cmd="0x11" button="8" group="8">LightOnSwitchHandler</message-handler>
<message-handler cmd="0x13" button="8" group="8">LightOffSwitchHandler</message-handler>
<poll-handler>NoPollHandler</poll-handler>
</feature>
<feature name="LoadSwitchButton">
<message-dispatcher>DefaultDispatcher</message-dispatcher>
<message-handler cmd="0x03" group="1">NoOpMsgHandler</message-handler>
<message-handler cmd="0x11" button="1" group="1">LightOnSwitchHandler</message-handler>
<message-handler cmd="0x12" button="1" group="1" mode="FAST">LightOnSwitchHandler</message-handler>
<message-handler cmd="0x13" button="1" group="1">LightOffSwitchHandler</message-handler>
<message-handler cmd="0x14" button="1" group="1" mode="FAST">LightOffSwitchHandler</message-handler>
<message-handler cmd="0x19" button="1" group="1">SwitchRequestReplyHandler</message-handler>
<command-handler command="OnOffType">LightOnOffCommandHandler</command-handler>
<poll-handler ext="0" cmd1="0x19" cmd2="0x00">FlexPollHandler</poll-handler>
</feature>
<feature name="LoadSwitchManualChange">
<message-dispatcher>DefaultDispatcher</message-dispatcher>
<message-handler cmd="0x17" group="1">StartManualChangeHandler</message-handler>
<message-handler cmd="0x18" group="1">StopManualChangeHandler</message-handler>
<command-handler command="DecimalType">ManualChangeCommandHandler</command-handler>
<poll-handler>NoPollHandler</poll-handler>
</feature>
<feature name="LoadSwitchFastOnOff">
<message-dispatcher>DefaultDispatcher</message-dispatcher>
<message-handler cmd="0x12" group="1" mode="FAST">LightOnSwitchHandler</message-handler>
<message-handler cmd="0x19" group="1">SwitchRequestReplyHandler</message-handler>
<message-handler cmd="0x14" group="1" mode="FAST">LightOffSwitchHandler</message-handler>
<command-handler command="OnOffType">FastOnOffCommandHandler</command-handler>
<poll-handler>NoPollHandler</poll-handler>
</feature>
<feature name="LoadDimmerButton">
<message-dispatcher>DefaultDispatcher</message-dispatcher>
<message-handler cmd="0x03" group="1">NoOpMsgHandler</message-handler>
<message-handler cmd="0x11" button="1" group="1">LightOnDimmerHandler</message-handler>
<message-handler cmd="0x12" button="1" group="1" mode="FAST">LightOnDimmerHandler</message-handler>
<message-handler cmd="0x13" button="1" group="1">LightOffDimmerHandler</message-handler>
<message-handler cmd="0x14" button="1" group="1" mode="FAST">LightOffDimmerHandler</message-handler>
<message-handler cmd="0x17" button="1" group="1">NoOpMsgHandler</message-handler>
<message-handler cmd="0x18" button="1" group="1">DimmerStopManualChangeHandler</message-handler>
<message-handler cmd="0x19" button="1" group="1">DimmerRequestReplyHandler</message-handler>
<command-handler command="PercentType">PercentHandler</command-handler>
<command-handler command="OnOffType">LightOnOffCommandHandler</command-handler>
<poll-handler ext="0" cmd1="0x19" cmd2="0x00">FlexPollHandler</poll-handler>
</feature>
<feature name="LoadDimmerFastOnOff">
<message-dispatcher>DefaultDispatcher</message-dispatcher>
<message-handler cmd="0x12" group="1" mode="FAST">LightOnDimmerHandler</message-handler>
<message-handler cmd="0x14" group="1" mode="FAST">LightOffDimmerHandler</message-handler>
<command-handler command="OnOffType">FastOnOffCommandHandler</command-handler>
<poll-handler>NoPollHandler</poll-handler>
</feature>
<feature name="LoadDimmerManualChange">
<message-dispatcher>DefaultDispatcher</message-dispatcher>
<message-handler cmd="0x17" group="1">StartManualChangeHandler</message-handler>
<message-handler cmd="0x18" group="1">StopManualChangeHandler</message-handler>
<command-handler command="DecimalType">ManualChangeCommandHandler</command-handler>
<poll-handler>NoPollHandler</poll-handler>
</feature>
<feature name="LoadDimmerRamp">
<message-dispatcher>DefaultDispatcher</message-dispatcher>
<!-- cmd1 defaults to 0x2E, 0x2F -->
<!-- default dispatcher uses 0x19 for lookup key instead of cmd1 -->
<message-handler cmd="0x19" group="1">RampDimmerHandler</message-handler>
<command-handler command="PercentType">RampPercentHandler</command-handler>
<command-handler command="OnOffType">RampOnOffCommandHandler</command-handler>
</feature>
<feature name="KeyPadButtonGroup">
<message-dispatcher>DefaultGroupDispatcher</message-dispatcher>
<poll-handler ext="0" cmd1="0x19" cmd2="0x01">FlexPollHandler</poll-handler>
</feature>
<feature name="FastOnOffButtonGroup">
<message-dispatcher>DefaultGroupDispatcher</message-dispatcher>
<poll-handler>NoPollHandler</poll-handler>
</feature>
<feature name="ManualChangeButtonGroup">
<message-dispatcher>DefaultGroupDispatcher</message-dispatcher>
<poll-handler>NoPollHandler</poll-handler>
</feature>
<feature name="KeyPadButton2">
<message-dispatcher>DefaultDispatcher</message-dispatcher>
<message-handler cmd="0x03" group="1">NoOpMsgHandler</message-handler>
<message-handler cmd="0x11" button="2" group="2">LightOnSwitchHandler</message-handler>
<message-handler cmd="0x12" button="2" group="2" mode="FAST">LightOnSwitchHandler</message-handler>
<message-handler cmd="0x13" button="2" group="2">LightOffSwitchHandler</message-handler>
<message-handler cmd="0x14" button="2" group="2" mode="FAST">LightOffSwitchHandler</message-handler>
<message-handler cmd="0x19" button="2" group="2">SwitchRequestReplyHandler</message-handler>
<command-handler command="OnOffType">LightOnOffCommandHandler</command-handler>
</feature>
<feature name="FastOnOffButton2">
<message-dispatcher>DefaultDispatcher</message-dispatcher>
<message-handler cmd="0x12" group="2" mode="FAST">LightOnSwitchHandler</message-handler>
<message-handler cmd="0x14" group="2" mode="FAST">LightOffSwitchHandler</message-handler>
<command-handler command="OnOffType">FastOnOffCommandHandler</command-handler>
<poll-handler>NoPollHandler</poll-handler>
</feature>
<feature name="ManualChangeButton2">
<message-dispatcher>DefaultDispatcher</message-dispatcher>
<message-handler cmd="0x17" group="2">StartManualChangeHandler</message-handler>
<message-handler cmd="0x18" group="2">StopManualChangeHandler</message-handler>
<command-handler command="DecimalType">ManualChangeCommandHandler</command-handler>
<poll-handler>NoPollHandler</poll-handler>
</feature>
<feature name="KeyPadButton3">
<message-dispatcher>DefaultDispatcher</message-dispatcher>
<message-handler cmd="0x03" group="1">NoOpMsgHandler</message-handler>
<message-handler cmd="0x11" button="3" group="3">LightOnSwitchHandler</message-handler>
<message-handler cmd="0x12" button="3" group="3" mode="FAST">LightOnSwitchHandler</message-handler>
<message-handler cmd="0x13" button="3" group="3">LightOffSwitchHandler</message-handler>
<message-handler cmd="0x14" button="3" group="3" mode="FAST">LightOffSwitchHandler</message-handler>
<message-handler cmd="0x19" button="3" group="3">SwitchRequestReplyHandler</message-handler>
<command-handler command="OnOffType">LightOnOffCommandHandler</command-handler>
</feature>
<feature name="FastOnOffButton3">
<message-dispatcher>DefaultDispatcher</message-dispatcher>
<message-handler cmd="0x12" group="3" mode="FAST">LightOnSwitchHandler</message-handler>
<message-handler cmd="0x14" group="3" mode="FAST">LightOffSwitchHandler</message-handler>
<command-handler command="OnOffType">FastOnOffCommandHandler</command-handler>
<poll-handler>NoPollHandler</poll-handler>
</feature>
<feature name="ManualChangeButton3">
<message-dispatcher>DefaultDispatcher</message-dispatcher>
<message-handler cmd="0x17" group="3">StartManualChangeHandler</message-handler>
<message-handler cmd="0x18" group="3">StopManualChangeHandler</message-handler>
<command-handler command="DecimalType">ManualChangeCommandHandler</command-handler>
<poll-handler>NoPollHandler</poll-handler>
</feature>
<feature name="KeyPadButton4">
<message-dispatcher>DefaultDispatcher</message-dispatcher>
<message-handler cmd="0x03" group="1">NoOpMsgHandler</message-handler>
<message-handler cmd="0x11" button="4" group="4">LightOnSwitchHandler</message-handler>
<message-handler cmd="0x12" button="4" group="4" mode="FAST">LightOnSwitchHandler</message-handler>
<message-handler cmd="0x13" button="4" group="4">LightOffSwitchHandler</message-handler>
<message-handler cmd="0x14" button="4" group="4" mode="FAST">LightOffSwitchHandler</message-handler>
<message-handler cmd="0x19" button="4" group="4">SwitchRequestReplyHandler</message-handler>
<command-handler command="OnOffType">LightOnOffCommandHandler</command-handler>
</feature>
<feature name="FastOnOffButton4">
<message-dispatcher>DefaultDispatcher</message-dispatcher>
<message-handler cmd="0x12" group="4" mode="FAST">LightOnSwitchHandler</message-handler>
<message-handler cmd="0x14" group="4" mode="FAST">LightOffSwitchHandler</message-handler>
<command-handler command="OnOffType">FastOnOffCommandHandler</command-handler>
<poll-handler>NoPollHandler</poll-handler>
</feature>
<feature name="ManualChangeButton4">
<message-dispatcher>DefaultDispatcher</message-dispatcher>
<message-handler cmd="0x17" group="4">StartManualChangeHandler</message-handler>
<message-handler cmd="0x18" group="4">StopManualChangeHandler</message-handler>
<command-handler command="DecimalType">ManualChangeCommandHandler</command-handler>
<poll-handler>NoPollHandler</poll-handler>
</feature>
<feature name="KeyPadButton5">
<message-dispatcher>DefaultDispatcher</message-dispatcher>
<message-handler cmd="0x03" group="1">NoOpMsgHandler</message-handler>
<message-handler cmd="0x11" button="5" group="5">LightOnSwitchHandler</message-handler>
<message-handler cmd="0x12" button="5" group="5" mode="FAST">LightOnSwitchHandler</message-handler>
<message-handler cmd="0x13" button="5" group="5">LightOffSwitchHandler</message-handler>
<message-handler cmd="0x14" button="5" group="5" mode="FAST">LightOffSwitchHandler</message-handler>
<message-handler cmd="0x19" button="5" group="5">SwitchRequestReplyHandler</message-handler>
<command-handler command="OnOffType">LightOnOffCommandHandler</command-handler>
</feature>
<feature name="FastOnOffButton5">
<message-dispatcher>DefaultDispatcher</message-dispatcher>
<message-handler cmd="0x12" group="5" mode="FAST">LightOnSwitchHandler</message-handler>
<message-handler cmd="0x14" group="5" mode="FAST">LightOffSwitchHandler</message-handler>
<command-handler command="OnOffType">FastOnOffCommandHandler</command-handler>
<poll-handler>NoPollHandler</poll-handler>
</feature>
<feature name="ManualChangeButton5">
<message-dispatcher>DefaultDispatcher</message-dispatcher>
<message-handler cmd="0x17" group="5">StartManualChangeHandler</message-handler>
<message-handler cmd="0x18" group="5">StopManualChangeHandler</message-handler>
<command-handler command="DecimalType">ManualChangeCommandHandler</command-handler>
<poll-handler>NoPollHandler</poll-handler>
</feature>
<feature name="KeyPadButton6">
<message-dispatcher>DefaultDispatcher</message-dispatcher>
<message-handler cmd="0x03" group="1">NoOpMsgHandler</message-handler>
<message-handler cmd="0x11" button="6" group="6">LightOnSwitchHandler</message-handler>
<message-handler cmd="0x12" button="6" group="6" mode="FAST">LightOnSwitchHandler</message-handler>
<message-handler cmd="0x13" button="6" group="6">LightOffSwitchHandler</message-handler>
<message-handler cmd="0x14" button="6" group="6" mode="FAST">LightOffSwitchHandler</message-handler>
<message-handler cmd="0x19" button="6" group="6">SwitchRequestReplyHandler</message-handler>
<command-handler command="OnOffType">LightOnOffCommandHandler</command-handler>
</feature>
<feature name="FastOnOffButton6">
<message-dispatcher>DefaultDispatcher</message-dispatcher>
<message-handler cmd="0x12" group="6" mode="FAST">LightOnSwitchHandler</message-handler>
<message-handler cmd="0x14" group="6" mode="FAST">LightOffSwitchHandler</message-handler>
<command-handler command="OnOffType">FastOnOffCommandHandler</command-handler>
<poll-handler>NoPollHandler</poll-handler>
</feature>
<feature name="ManualChangeButton6">
<message-dispatcher>DefaultDispatcher</message-dispatcher>
<message-handler cmd="0x17" group="6">StartManualChangeHandler</message-handler>
<message-handler cmd="0x18" group="6">StopManualChangeHandler</message-handler>
<command-handler command="DecimalType">ManualChangeCommandHandler</command-handler>
<poll-handler>NoPollHandler</poll-handler>
</feature>
<feature name="KeyPadButton7">
<message-dispatcher>DefaultDispatcher</message-dispatcher>
<message-handler cmd="0x03" group="1">NoOpMsgHandler</message-handler>
<message-handler cmd="0x11" button="7" group="7">LightOnSwitchHandler</message-handler>
<message-handler cmd="0x12" button="7" group="7" mode="FAST">LightOnSwitchHandler</message-handler>
<message-handler cmd="0x13" button="7" group="7">LightOffSwitchHandler</message-handler>
<message-handler cmd="0x14" button="7" group="7" mode="FAST">LightOffSwitchHandler</message-handler>
<message-handler cmd="0x19" button="7" group="7">SwitchRequestReplyHandler</message-handler>
<command-handler command="OnOffType">LightOnOffCommandHandler</command-handler>
</feature>
<feature name="FastOnOffButton7">
<message-dispatcher>DefaultDispatcher</message-dispatcher>
<message-handler cmd="0x12" group="7" mode="FAST">LightOnSwitchHandler</message-handler>
<message-handler cmd="0x14" group="7" mode="FAST">LightOffSwitchHandler</message-handler>
<command-handler command="OnOffType">FastOnOffCommandHandler</command-handler>
<poll-handler>NoPollHandler</poll-handler>
</feature>
<feature name="ManualChangeButton7">
<message-dispatcher>DefaultDispatcher</message-dispatcher>
<message-handler cmd="0x17" group="7">StartManualChangeHandler</message-handler>
<message-handler cmd="0x18" group="7">StopManualChangeHandler</message-handler>
<command-handler command="DecimalType">ManualChangeCommandHandler</command-handler>
<poll-handler>NoPollHandler</poll-handler>
</feature>
<feature name="KeyPadButton8">
<message-dispatcher>DefaultDispatcher</message-dispatcher>
<message-handler cmd="0x03" group="1">NoOpMsgHandler</message-handler>
<message-handler cmd="0x11" button="8" group="8">LightOnSwitchHandler</message-handler>
<message-handler cmd="0x12" button="8" group="8" mode="FAST">LightOnSwitchHandler</message-handler>
<message-handler cmd="0x13" button="8" group="8">LightOffSwitchHandler</message-handler>
<message-handler cmd="0x14" button="8" group="8" mode="FAST">LightOffSwitchHandler</message-handler>
<message-handler cmd="0x19" button="8" group="8">SwitchRequestReplyHandler</message-handler>
<command-handler command="OnOffType">LightOnOffCommandHandler</command-handler>
</feature>
<feature name="FastOnOffButton8">
<message-dispatcher>DefaultDispatcher</message-dispatcher>
<message-handler cmd="0x12" group="8" mode="FAST">LightOnSwitchHandler</message-handler>
<message-handler cmd="0x14" group="8" mode="FAST">LightOffSwitchHandler</message-handler>
<command-handler command="OnOffType">FastOnOffCommandHandler</command-handler>
<poll-handler>NoPollHandler</poll-handler>
</feature>
<feature name="ManualChangeButton8">
<message-dispatcher>DefaultDispatcher</message-dispatcher>
<message-handler cmd="0x17" group="8">StartManualChangeHandler</message-handler>
<message-handler cmd="0x18" group="8">StopManualChangeHandler</message-handler>
<command-handler command="DecimalType">ManualChangeCommandHandler</command-handler>
<poll-handler>NoPollHandler</poll-handler>
</feature>
<feature name="GenericLastTime" statusFeature="true">
<message-dispatcher>PassThroughDispatcher</message-dispatcher>
<message-handler default="true">LastTimeHandler</message-handler>
<command-handler default="true">NoOpCommandHandler</command-handler>
</feature>
<feature name="GenericDimmer">
<message-dispatcher>DefaultDispatcher</message-dispatcher>
<message-handler cmd="0x03" group="1">NoOpMsgHandler</message-handler>
<message-handler cmd="0x11" group="1">LightOnDimmerHandler</message-handler>
<message-handler cmd="0x12" group="1" mode="FAST">LightOnDimmerHandler</message-handler>
<message-handler cmd="0x13" group="1">LightOffDimmerHandler</message-handler>
<message-handler cmd="0x14" group="1" mode="FAST">LightOffDimmerHandler</message-handler>
<message-handler cmd="0x17" group="1">NoOpMsgHandler</message-handler>
<message-handler cmd="0x18" group="1">DimmerStopManualChangeHandler</message-handler>
<message-handler cmd="0x19">DimmerRequestReplyHandler</message-handler>
<command-handler command="PercentType">PercentHandler</command-handler>
<command-handler command="IncreaseDecreaseType">IncreaseDecreaseCommandHandler</command-handler>
<command-handler command="OnOffType">LightOnOffCommandHandler</command-handler>
<poll-handler ext="0" cmd1="0x19" cmd2="0x00">FlexPollHandler</poll-handler>
</feature>
<feature name="IOLincContact">
<message-dispatcher>DefaultDispatcher</message-dispatcher>
<message-handler cmd="0x03" group="1">NoOpMsgHandler</message-handler>
<message-handler cmd="0x11" group="1">OpenedContactHandler</message-handler>
<message-handler cmd="0x13" group="1">ClosedContactHandler</message-handler>
<message-handler cmd="0x19">ContactRequestReplyHandler</message-handler>
<command-handler command="OnOffType">NoOpCommandHandler</command-handler>
<poll-handler ext="0" cmd1="0x19" cmd2="0x01">FlexPollHandler</poll-handler>
</feature>
<feature name="IOLincSwitch">
<message-dispatcher>DefaultDispatcher</message-dispatcher>
<message-handler cmd="0x03" group="1">NoOpMsgHandler</message-handler>
<message-handler cmd="0x11" group="1">NoOpMsgHandler</message-handler>
<message-handler cmd="0x13" group="1">NoOpMsgHandler</message-handler>
<message-handler cmd="0x19">SwitchRequestReplyHandler</message-handler>
<command-handler command="OnOffType">IOLincOnOffCommandHandler</command-handler>
<poll-handler ext="0" cmd1="0x19" cmd2="0x00">FlexPollHandler</poll-handler>
</feature>
<feature name="WirelessMotionSensorContact">
<message-dispatcher>DefaultDispatcher</message-dispatcher>
<message-handler cmd="0x03" group="1">NoOpMsgHandler</message-handler>
<message-handler cmd="0x11" group="1">OpenedSleepingContactHandler</message-handler>
<message-handler cmd="0x13" group="1">ClosedSleepingContactHandler</message-handler>
<message-handler cmd="0x19">NoOpMsgHandler</message-handler>
<message-handler cmd="0x2e">NoOpMsgHandler</message-handler>
<command-handler command="OnOffType">NoOpCommandHandler</command-handler>
<poll-handler>NoPollHandler</poll-handler>
</feature>
<feature name="WirelessMotionSensorLightLevelAboveThreshold">
<message-dispatcher>DefaultDispatcher</message-dispatcher>
<message-handler cmd="0x03" group="2">NoOpMsgHandler</message-handler>
<message-handler cmd="0x11" group="2">OpenedSleepingContactHandler</message-handler>
<message-handler cmd="0x13" group="2">ClosedSleepingContactHandler</message-handler>
<message-handler cmd="0x19">NoOpMsgHandler</message-handler>
<message-handler cmd="0x2e">NoOpMsgHandler</message-handler>
<command-handler command="OnOffType">NoOpCommandHandler</command-handler>
<poll-handler>NoPollHandler</poll-handler>
</feature>
<feature name="WirelessMotionSensorLowBattery">
<message-dispatcher>DefaultDispatcher</message-dispatcher>
<message-handler cmd="0x03" group="3">NoOpMsgHandler</message-handler>
<message-handler cmd="0x11" group="3">OpenedSleepingContactHandler</message-handler>
<message-handler cmd="0x13" group="3">ClosedSleepingContactHandler</message-handler>
<message-handler cmd="0x19">NoOpMsgHandler</message-handler>
<message-handler cmd="0x2e">NoOpMsgHandler</message-handler>
<command-handler command="OnOffType">NoOpCommandHandler</command-handler>
<poll-handler>NoPollHandler</poll-handler>
</feature>
<feature name="WirelessMotionSensor2TamperSwitch">
<message-dispatcher>DefaultDispatcher</message-dispatcher>
<message-handler cmd="0x03" group="16">NoOpMsgHandler</message-handler>
<message-handler cmd="0x11" group="16">OpenedSleepingContactHandler</message-handler>
<message-handler cmd="0x13" group="16">ClosedSleepingContactHandler</message-handler>
<message-handler cmd="0x19">NoOpMsgHandler</message-handler>
<message-handler cmd="0x2e">NoOpMsgHandler</message-handler>
<command-handler command="OnOffType">NoOpCommandHandler</command-handler>
<poll-handler>NoPollHandler</poll-handler>
</feature>
<feature name="MotionSensorData">
<message-dispatcher>SimpleDispatcher</message-dispatcher>
<message-handler cmd="0x03" group="1">NoOpMsgHandler</message-handler>
<message-handler cmd="0x11" group="1">NoOpMsgHandler</message-handler>
<message-handler cmd="0x13" group="1">NoOpMsgHandler</message-handler>
<message-handler cmd="0x2e">MotionSensorDataReplyHandler</message-handler>
<command-handler command="OnOffType">NoOpCommandHandler</command-handler>
<poll-handler>NoPollHandler</poll-handler>
</feature>
<feature name="MotionSensor2Data">
<message-dispatcher>SimpleDispatcher</message-dispatcher>
<message-handler cmd="0x03" group="1">NoOpMsgHandler</message-handler>
<message-handler cmd="0x0C" group="1">NoOpMsgHandler</message-handler>
<message-handler cmd="0x11" group="1">NoOpMsgHandler</message-handler>
<message-handler cmd="0x13" group="1">NoOpMsgHandler</message-handler>
<message-handler cmd="0x03" group="11">NoOpMsgHandler</message-handler>
<message-handler cmd="0x0C" group="11">MotionSensor2AlternateHeartbeatHandler</message-handler>
<message-handler cmd="0x11" group="11">NoOpMsgHandler</message-handler>
<message-handler cmd="0x13" group="11">NoOpMsgHandler</message-handler>
<message-handler cmd="0x2e">MotionSensorDataReplyHandler</message-handler>
<command-handler command="OnOffType">NoOpCommandHandler</command-handler>
<poll-handler>NoPollHandler</poll-handler>
</feature>
<feature name="HiddenDoorSensorData">
<message-dispatcher>SimpleDispatcher</message-dispatcher>
<message-handler cmd="0x03" group="1">NoOpMsgHandler</message-handler>
<message-handler cmd="0x11" group="1">NoOpMsgHandler</message-handler>
<message-handler cmd="0x13" group="1">NoOpMsgHandler</message-handler>
<message-handler cmd="0x2e">HiddenDoorSensorDataReplyHandler</message-handler>
<command-handler command="OnOffType">NoOpCommandHandler</command-handler>
<poll-handler>NoPollHandler</poll-handler>
</feature>
<feature name="GenericContact">
<message-dispatcher>DefaultDispatcher</message-dispatcher>
<message-handler cmd="0x03" group="1">NoOpMsgHandler</message-handler>
<message-handler cmd="0x11" group="1">OpenedContactHandler</message-handler>
<message-handler cmd="0x13" group="1">ClosedContactHandler</message-handler>
<command-handler command="OnOffType">NoOpCommandHandler</command-handler>
<poll-handler>NoPollHandler</poll-handler>
</feature>
<feature name="LeakSensorContact">
<message-dispatcher>DefaultDispatcher</message-dispatcher>
<message-handler cmd="0x03">NoOpMsgHandler</message-handler>
<message-handler cmd="0x11">OpenedOrClosedContactHandler</message-handler>
<message-handler cmd="0x13">OpenedOrClosedContactHandler</message-handler>
<command-handler command="OnOffType">NoOpCommandHandler</command-handler>
<poll-handler>NoPollHandler</poll-handler>
</feature>
<feature name="GroupBroadcastOnOff">
<message-dispatcher>NoOpDispatcher</message-dispatcher>
<command-handler command="OnOffType">GroupBroadcastCommandHandler</command-handler>
<message-handler default="true">NoOpMsgHandler</message-handler>
</feature>
<feature name="PowerMeter">
<message-dispatcher>SimpleDispatcher</message-dispatcher>
<message-handler cmd="0x03">NoOpMsgHandler</message-handler>
<message-handler cmd="0x80">PowerMeterResetHandler</message-handler>
<message-handler cmd="0x82">PowerMeterUpdateHandler</message-handler>
<command-handler command="OnOffType">PowerMeterCommandHandler</command-handler>
<poll-handler ext="0" cmd1="0x82" cmd2="0x00">FlexPollHandler</poll-handler>
</feature>
<feature name="X10Dimmer" timeout="0">
<message-dispatcher>X10Dispatcher</message-dispatcher>
<message-handler cmd="0x02">X10OnHandler</message-handler>
<message-handler cmd="0x03">X10OffHandler</message-handler>
<message-handler cmd="0x05">X10BrightHandler</message-handler>
<message-handler cmd="0x04">X10DimHandler</message-handler>
<message-handler default="true">NoOpMsgHandler</message-handler>
<command-handler command="OnOffType">X10OnOffCommandHandler</command-handler>
<command-handler command="PercentType">X10PercentCommandHandler</command-handler>
<command-handler command="IncreaseDecreaseType">X10IncreaseDecreaseCommandHandler</command-handler>
<poll-handler>NoPollHandler</poll-handler>
</feature>
<feature name="X10Switch" timeout="0">
<message-dispatcher>X10Dispatcher</message-dispatcher>
<message-handler cmd="0x02">X10OnHandler</message-handler>
<message-handler cmd="0x03">X10OffHandler</message-handler>
<message-handler default="true">NoOpMsgHandler</message-handler>
<command-handler command="OnOffType">X10OnOffCommandHandler</command-handler>
<command-handler command="PercentType">NoOpCommandHandler</command-handler>
<command-handler command="IncreaseDecreaseType">NoOpCommandHandler</command-handler>
<poll-handler>NoPollHandler</poll-handler>
</feature>
<feature name="X10Contact">
<message-dispatcher>X10Dispatcher</message-dispatcher>
<message-handler cmd="0x02">X10OpenHandler</message-handler>
<message-handler cmd="0x03">X10ClosedHandler</message-handler>
<message-handler default="true">NoOpMsgHandler</message-handler>
<command-handler default="true">NoOpCommandHandler</command-handler>
<poll-handler>NoPollHandler</poll-handler>
</feature>
<feature name="ThermostatData1Group"> <!-- just does the polling for various quantities -->
<message-dispatcher>PollGroupDispatcher</message-dispatcher>
<poll-handler ext="1" cmd1="0x2e" cmd2="0x00">FlexPollHandler</poll-handler>
</feature>
<feature name="ThermostatData1bGroup"> <!-- just does the polling for various quantities -->
<message-dispatcher>PollGroupDispatcher</message-dispatcher>
<poll-handler ext="2" cmd1="0x2e" cmd2="0x00" d3="0x01">FlexPollHandler</poll-handler>
</feature>
<feature name="ThermostatData2Group"> <!-- just does the polling for various quantities -->
<message-dispatcher>PollGroupDispatcher</message-dispatcher>
<poll-handler ext="2" cmd1="0x2e" cmd2="0x02">FlexPollHandler</poll-handler>
</feature>
<feature name="ThermostatCoolSetPoint">
<message-dispatcher>DefaultDispatcher</message-dispatcher>
<!-- handles direct extended message after query -->
<message-handler cmd="0x2e" ext="1" match_cmd1="0x2e" match_cmd2="0x02" match_d1="0x01"
low_byte="userData7">NumberMsgHandler</message-handler>
<!-- handles direct ack after set point has been changed -->
<message-handler cmd="0x19" ext="0" match_cmd1="0x6c" low_byte="command2" factor="0.5">NumberMsgHandler</message-handler>
<!-- handles out-of band status messages -->
<message-handler cmd="0x71" ext="0" match_cmd1="0x71" low_byte="command2">NumberMsgHandler</message-handler>
<message-handler default="true">NoOpMsgHandler</message-handler>
<command-handler command="DecimalType" ext="1" cmd1="0x6c" factor="2" value="command2">NumberCommandHandler</command-handler>
<command-handler default="true">NoOpCommandHandler</command-handler>
<poll-handler>NoPollHandler</poll-handler> <!-- polled by ThermostatData2Group -->
</feature>
<feature name="ThermostatHeatSetPoint">
<message-dispatcher>DefaultDispatcher</message-dispatcher>
<!-- handles direct extended message after query -->
<message-handler cmd="0x2e" ext="1" match_cmd1="0x2e" match_cmd2="0x02" match_d1="0x01"
low_byte="userData12">NumberMsgHandler</message-handler>
<!-- handles direct ack after set point has been changed -->
<message-handler cmd="0x19" ext="0" match_cmd1="0x6d" low_byte="command2" factor="0.5">NumberMsgHandler</message-handler>
<!-- handles out-of band status messages -->
<message-handler cmd="0x72" ext="0" match_cmd1="0x72" low_byte="command2">NumberMsgHandler</message-handler>
<message-handler default="true">NoOpMsgHandler</message-handler>
<command-handler command="DecimalType" ext="1" cmd1="0x6d" factor="2" value="command2">NumberCommandHandler</command-handler>
<command-handler default="true">NoOpCommandHandler</command-handler>
<poll-handler>NoPollHandler</poll-handler> <!-- polled by ThermostatData2Group -->
</feature>
<feature name="ThermostatSystemMode">
<message-dispatcher>DefaultDispatcher</message-dispatcher>
<!-- handles direct extended message after query -->
<message-handler cmd="0x2e" ext="1" match_cmd1="0x2e" match_cmd2="0x02" match_d1="0x01"
low_byte="userData6" mask="0xf0" rshift="4">ThermostatSystemModeMsgHandler</message-handler>
<!-- handles direct ack after system mode has been changed -->
<message-handler cmd="0x19" ext="0" match_cmd1="0x6b" low_byte="command2">ThermostatSystemModeReplyHandler</message-handler>
<!-- handles out-of band status messages -->
<message-handler cmd="0x70" ext="0" match_cmd1="0x70" low_byte="command2" mask="0x0f">NumberMsgHandler</message-handler>
<message-handler default="true">NoOpMsgHandler</message-handler>
<command-handler command="DecimalType" ext="1" cmd1="0x6b" value="command2">ThermostatSystemModeCommandHandler</command-handler>
<command-handler default="true">NoOpCommandHandler</command-handler>
<poll-handler>NoPollHandler</poll-handler> <!-- polled by ThermostatData2Group -->
</feature>
<feature name="ThermostatFanMode">
<message-dispatcher>DefaultDispatcher</message-dispatcher>
<!-- handles direct extended message after query -->
<message-handler cmd="0x2e" ext="1" match_cmd1="0x2e" match_cmd2="0x02" match_d1="0x01"
low_byte="userData6" mask="0x0f">NumberMsgHandler</message-handler>
<!-- handles direct ack after fan mode has been changed -->
<message-handler cmd="0x19" ext="0" match_cmd1="0x6b" low_byte="command2">ThermostatFanModeReplyHandler</message-handler>
<!-- handles out-of band status messages -->
<message-handler cmd="0x70" ext="0" match_cmd1="0x70" low_byte="command2" mask="0xf0" rshift="4">NumberMsgHandler</message-handler>
<message-handler default="true">NoOpMsgHandler</message-handler>
<command-handler command="DecimalType" ext="1" cmd1="0x6b" value="command2">ThermostatFanModeCommandHandler</command-handler>
<command-handler default="true">NoOpCommandHandler</command-handler>
<poll-handler>NoPollHandler</poll-handler> <!-- polled by ThermostatData2Group -->
</feature>
<feature name="ThermostatIsHeating">
<message-dispatcher>DefaultDispatcher</message-dispatcher>
<!-- handles direct extended message after query -->
<message-handler cmd="0x2e" ext="1" match_cmd1="0x2e" match_cmd2="0x02" match_d1="0x01"
low_byte="userData11" mask="0x02" rshift="1">NumberMsgHandler</message-handler>
<!-- handles all-link broadcast message OFF -->
<message-handler cmd="0x13" ext="0" group="2" mask="0x00" low_byte="command1">NumberMsgHandler</message-handler>
<!-- handles all-link broadcast message ON -->
<message-handler cmd="0x11" ext="0" group="2" mask="0x01" low_byte="command1">NumberMsgHandler</message-handler>
<message-handler default="true">NoOpMsgHandler</message-handler>
<command-handler default="true">NoOpCommandHandler</command-handler>
<poll-handler>NoPollHandler</poll-handler> <!-- polled by ThermostatData2Group -->
</feature>
<feature name="ThermostatIsCooling">
<message-dispatcher>DefaultDispatcher</message-dispatcher>
<!-- handles direct extended message after query -->
<message-handler cmd="0x2e" ext="1" match_cmd1="0x2e" match_cmd2="0x02" match_d1="0x01"
low_byte="userData11" mask="0x01">NumberMsgHandler</message-handler>
<!-- handles all-link broadcast message OFF -->
<message-handler cmd="0x13" ext="0" group="1" mask="0x00" low_byte="command1">NumberMsgHandler</message-handler>
<!-- handles all-link broadcast message ON -->
<message-handler cmd="0x11" ext="0" group="1" mask="0x01" low_byte="command1">NumberMsgHandler</message-handler>
<message-handler default="true">NoOpMsgHandler</message-handler>
<command-handler default="true">NoOpCommandHandler</command-handler>
<poll-handler>NoPollHandler</poll-handler> <!-- polled by ThermostatData2Group -->
</feature>
<feature name="ThermostatTemperatureCelsius">
<message-dispatcher>DefaultDispatcher</message-dispatcher>
<!-- handles direct extended message after query -->
<message-handler cmd="0x2e" ext="1" match_cmd1="0x2e" match_cmd2="0x02" match_d1="0x01"
low_byte="userData10" high_byte="userData9" factor="0.1">NumberMsgHandler</message-handler>
<!-- handles out-of band status messages -->
<message-handler cmd="0x6e" ext="0" match_cmd1="0x6e" low_byte="command2" offset="-17.7777778"
factor="0.2777778" scale="celsius">NumberMsgHandler</message-handler>
<message-handler default="true">NoOpMsgHandler</message-handler>
<command-handler default="true">NoOpCommandHandler</command-handler>
<poll-handler>NoPollHandler</poll-handler> <!-- polled by ThermostatData2Group -->
</feature>
<feature name="ThermostatTemperatureFahrenheit">
<message-dispatcher>DefaultDispatcher</message-dispatcher>
<!-- handles direct extended message after query -->
<message-handler cmd="0x2e" ext="1" match_cmd1="0x2e" match_cmd2="0x02" match_d1="0x01"
low_byte="userData10" high_byte="userData9" offset="32" factor="0.18">NumberMsgHandler</message-handler>
<!-- handles out-of band status messages -->
<message-handler cmd="0x6e" ext="0" match_cmd1="0x6e" low_byte="command2" offset="0" factor="0.5"
scale="fahrenheit">NumberMsgHandler</message-handler>
<message-handler default="true">NoOpMsgHandler</message-handler>
<command-handler default="true">NoOpCommandHandler</command-handler>
<poll-handler>NoPollHandler</poll-handler> <!-- polled by ThermostatData2Group -->
</feature>
<feature name="ThermostatHumidity">
<message-dispatcher>DefaultDispatcher</message-dispatcher>
<!-- handles direct extended message after query -->
<message-handler cmd="0x2e" ext="1" match_cmd1="0x2e" match_cmd2="0x02" match_d1="0x01"
low_byte="userData8">NumberMsgHandler</message-handler>
<!-- handles out-of band status messages -->
<message-handler cmd="0x6f" ext="0" match_cmd1="0x6f" low_byte="command2">NumberMsgHandler</message-handler>
<message-handler default="true">NoOpMsgHandler</message-handler>
<command-handler default="true">NoOpCommandHandler</command-handler>
<poll-handler>NoPollHandler</poll-handler> <!-- polled by ThermostatData2Group -->
</feature>
<feature name="ThermostatBackLightDuration">
<message-dispatcher>DefaultDispatcher</message-dispatcher>
<!-- handles direct extended message after query -->
<message-handler cmd="0x2e" ext="1" match_cmd1="0x2e" match_cmd2="0x00" match_d2="0x01"
match_d3="0x00" low_byte="userData10">NumberMsgHandler</message-handler>
<!-- handles direct ack after backlight duration has been changed -->
<message-handler cmd="0x19">TriggerPollMsgHandler</message-handler>
<message-handler default="true">NoOpMsgHandler</message-handler>
<command-handler command="DecimalType" ext="1" cmd1="0x2e" d1="0x01" d2="0x05" value="userData3">NumberCommandHandler</command-handler>
<command-handler default="true">NoOpCommandHandler</command-handler>
<poll-handler>NoPollHandler</poll-handler> <!-- polled by ThermostatData1Group -->
</feature>
<feature name="ThermostatACDelay">
<message-dispatcher>DefaultDispatcher</message-dispatcher>
<!-- handles direct extended message after query -->
<message-handler cmd="0x2e" ext="1" match_cmd1="0x2e" match_cmd2="0x00" match_d2="0x01"
match_d3="0x00" low_byte="userData11">NumberMsgHandler</message-handler>
<!-- handles direct ack after backlight duration has been changed -->
<message-handler cmd="0x19">TriggerPollMsgHandler</message-handler>
<message-handler default="true">NoOpMsgHandler</message-handler>
<command-handler command="DecimalType" ext="1" cmd1="0x2e" d1="0x01" d2="0x06" value="userData3">NumberCommandHandler</command-handler>
<command-handler default="true">NoOpCommandHandler</command-handler>
<poll-handler>NoPollHandler</poll-handler> <!-- polled by ThermostatData1Group -->
</feature>
<feature name="ThermostatHumidityHigh">
<message-dispatcher>DefaultDispatcher</message-dispatcher>
<!-- handles direct extended message after query -->
<message-handler cmd="0x2e" ext="1" match_cmd1="0x2e" match_cmd2="0x00" match_d2="0x01"
match_d3="0x01" low_byte="userData4">NumberMsgHandler</message-handler>
<!-- handles direct ack after value has been changed -->
<message-handler cmd="0x19">TriggerPollMsgHandler</message-handler>
<message-handler default="true">NoOpMsgHandler</message-handler>
<command-handler command="DecimalType" ext="1" cmd1="0x2e" d1="0x01" d2="0x0b" value="userData3">NumberCommandHandler</command-handler>
<command-handler default="true">NoOpCommandHandler</command-handler>
<poll-handler>NoPollHandler</poll-handler> <!-- polled by ThermostatData1bGroup -->
</feature>
<feature name="ThermostatHumidityLow">
<message-dispatcher>DefaultDispatcher</message-dispatcher>
<!-- handles direct extended message after query -->
<message-handler cmd="0x2e" ext="1" match_cmd1="0x2e" match_cmd2="0x00" match_d2="0x01"
match_d3="0x01" low_byte="userData5">NumberMsgHandler</message-handler>
<!-- handles direct ack after value has been changed -->
<message-handler cmd="0x19">TriggerPollMsgHandler</message-handler>
<message-handler default="true">NoOpMsgHandler</message-handler>
<command-handler command="DecimalType" ext="1" cmd1="0x2e" d1="0x01" d2="0x0c" value="userData3">NumberCommandHandler</command-handler>
<command-handler default="true">NoOpCommandHandler</command-handler>
<poll-handler>NoPollHandler</poll-handler> <!-- polled by ThermostatData1bGroup -->
</feature>
<feature name="ThermostatStage1Duration">
<message-dispatcher>DefaultDispatcher</message-dispatcher>
<!-- handles direct extended message after query -->
<message-handler cmd="0x2e" ext="1" match_cmd1="0x2e" match_cmd2="0x00" match_d2="0x01"
match_d3="0x01" low_byte="userData11">NumberMsgHandler</message-handler>
<!-- handles direct ack after value has been changed -->
<message-handler cmd="0x19">TriggerPollMsgHandler</message-handler>
<message-handler default="true">NoOpMsgHandler</message-handler>
<command-handler command="DecimalType" ext="1" cmd1="0x2e" d1="0x01" d2="0x0a" value="userData3">NumberCommandHandler</command-handler>
<command-handler default="true">NoOpCommandHandler</command-handler>
<poll-handler>NoPollHandler</poll-handler> <!-- polled by ThermostatData1bGroup -->
</feature>
<feature name="FanLincFan">
<message-dispatcher>DefaultDispatcher</message-dispatcher>
<message-handler cmd="0x19" ext="0" low_byte="command2">FanLincFanReplyHandler</message-handler>
<command-handler command="DecimalType" ext="1" cmd1="0x11" d1="0x02" value="command2">FanLincFanCommandHandler</command-handler>
<poll-handler ext="0" cmd1="0x19" cmd2="0x03">FlexPollHandler</poll-handler>
</feature>
<feature name="BottomOutlet">
<message-dispatcher>DefaultDispatcher</message-dispatcher>
<message-handler cmd="0x19" button="2" group="1">SwitchRequestReplyHandler</message-handler>
<command-handler d1="0x02" ext="1" command="OnOffType">LightOnOffCommandHandler</command-handler>
<poll-handler ext="0" cmd1="0x19" cmd2="0x01">FlexPollHandler</poll-handler>
</feature>
<feature name="VenstarCoolSetPoint">
<message-dispatcher>DefaultDispatcher</message-dispatcher>
<!-- handles direct extended message after query -->
<message-handler cmd="0x2e" ext="1" match_cmd1="0x2e" match_cmd2="0x00" low_byte="userData6">NumberMsgHandler</message-handler>
<!-- handles direct ack after set point has been changed -->
<message-handler cmd="0x19" ext="0" match_cmd1="0x6c" low_byte="command2" factor="0.5">NumberMsgHandler</message-handler>
<!-- handles out-of band status messages -->
<message-handler cmd="0x71" ext="0" match_cmd1="0x71" low_byte="command2">NumberMsgHandler</message-handler>
<message-handler default="true">NoOpMsgHandler</message-handler>
<command-handler command="DecimalType" ext="1" cmd1="0x6c" factor="2" value="command2">NumberCommandHandler</command-handler>
<command-handler default="true">NoOpCommandHandler</command-handler>
<poll-handler>NoPollHandler</poll-handler> <!-- polled by ThermostatData1bGroup -->
</feature>
<feature name="VenstarHeatSetPoint">
<message-dispatcher>DefaultDispatcher</message-dispatcher>
<!-- handles direct extended message after query -->
<message-handler cmd="0x2e" ext="1" match_cmd1="0x2e" match_cmd2="0x00" low_byte="userData7">NumberMsgHandler</message-handler>
<!-- handles direct ack after set point has been changed -->
<message-handler cmd="0x19" ext="0" match_cmd1="0x6d" low_byte="command2" factor="0.5">NumberMsgHandler</message-handler>
<!-- handles out-of band status messages -->
<message-handler cmd="0x72" ext="0" match_cmd1="0x72" low_byte="command2">NumberMsgHandler</message-handler>
<message-handler default="true">NoOpMsgHandler</message-handler>
<command-handler command="DecimalType" ext="1" cmd1="0x6d" factor="2" value="command2">NumberCommandHandler</command-handler>
<command-handler default="true">NoOpCommandHandler</command-handler>
<poll-handler>NoPollHandler</poll-handler> <!-- polled by ThermostatData1bGroup -->
</feature>
<feature name="VenstarSystemMode">
<message-dispatcher>DefaultDispatcher</message-dispatcher>
<!-- handles direct extended message after query - use NumberMsgHandler because this adapator directly reports the correct
number -->
<!-- 0=OFF, 1=HEAT, 2=COOL, 3= Auto, 4=Program 5=Program Heat 6=Program Cool -->
<message-handler cmd="0x2e" ext="1" match_cmd1="0x2e" match_cmd2="0x00" match_d2="0x01"
low_byte="userData3">NumberMsgHandler</message-handler>
<!-- handles direct ack after system mode has been changed -->
<message-handler cmd="0x19" ext="0" match_cmd1="0x6b" low_byte="command2">ThermostatSystemModeReplyHandler</message-handler>
<!-- handles out-of band status messages -->
<message-handler cmd="0x70" ext="0" match_cmd1="0x70" low_byte="command2" mask="0x0f">NumberMsgHandler</message-handler>
<message-handler default="true">NoOpMsgHandler</message-handler>
<command-handler command="DecimalType" ext="1" cmd1="0x6b" value="command2">ThermostatSystemModeCommandHandler</command-handler>
<command-handler default="true">NoOpCommandHandler</command-handler>
<poll-handler>NoPollHandler</poll-handler> <!-- polled by ThermostatData1bGroup -->
</feature>
<feature name="VenstarFanMode">
<message-dispatcher>DefaultDispatcher</message-dispatcher>
<!-- handles direct extended message after query mask for second bit -->
<message-handler cmd="0x2e" ext="1" match_cmd1="0x2e" match_cmd2="0x00" low_byte="userData9"
mask="0x10" rshift="4">NumberMsgHandler</message-handler>
<!-- handles direct ack after fan mode has been changed -->
<message-handler cmd="0x19" ext="0" match_cmd1="0x6b" low_byte="command2">ThermostatFanModeReplyHandler</message-handler>
<!-- handles out-of band status messages -->
<message-handler cmd="0x70" ext="0" match_cmd1="0x70" low_byte="command2" mask="0xf0" rshift="4">NumberMsgHandler</message-handler>
<message-handler default="true">NoOpMsgHandler</message-handler>
<command-handler command="DecimalType" ext="1" cmd1="0x6b" value="command2">ThermostatFanModeCommandHandler</command-handler>
<command-handler default="true">NoOpCommandHandler</command-handler>
<poll-handler>NoPollHandler</poll-handler> <!-- polled by ThermostatData1bGroup -->
</feature>
<feature name="VenstarTemperatureFahrenheit"> <!-- All temperatures reported in units currently set on thermostat -->
<message-dispatcher>DefaultDispatcher</message-dispatcher>
<!-- handles direct extended message after query -->
<message-handler cmd="0x2e" ext="1" match_cmd1="0x2e" match_cmd2="0x00" match_d2="0x01"
low_byte="userData5">NumberMsgHandler</message-handler>
<!-- handles direct ack after value has been changed -->
<message-handler cmd="0x19">TriggerPollMsgHandler</message-handler>
<message-handler default="true">NoOpMsgHandler</message-handler>
<command-handler command="DecimalType" ext="1" cmd1="0x2e" d1="0x01" d2="0x0b" value="userData3">NumberCommandHandler</command-handler>
<command-handler default="true">NoOpCommandHandler</command-handler>
<poll-handler>NoPollHandler</poll-handler> <!-- polled by ThermostatData1bGroup -->
</feature>
<feature name="VenstarHumidity">
<message-dispatcher>DefaultDispatcher</message-dispatcher>
<!-- handles direct extended message after query -->
<message-handler cmd="0x2e" ext="1" match_cmd1="0x2e" match_cmd2="0x00" match_d2="0x01"
low_byte="userData4">NumberMsgHandler</message-handler>
<!-- handles direct ack after value has been changed -->
<message-handler cmd="0x19">TriggerPollMsgHandler</message-handler>
<message-handler default="true">NoOpMsgHandler</message-handler>
<command-handler command="DecimalType" ext="1" cmd1="0x2e" d1="0x01" d2="0x0c" value="userData3">NumberCommandHandler</command-handler>
<command-handler default="true">NoOpCommandHandler</command-handler>
<poll-handler>NoPollHandler</poll-handler> <!-- polled by ThermostatData1bGroup -->
</feature>
<feature name="VenstarIsHeating">
<message-dispatcher>DefaultDispatcher</message-dispatcher>
<!-- handles direct extended message after query -->
<message-handler cmd="0x2e" ext="1" match_cmd1="0x2e" match_cmd2="0x00" low_byte="userData8"
mask="0x02" rshift="1">NumberMsgHandler</message-handler>
<!-- handles all-link broadcast message OFF -->
<message-handler cmd="0x13" ext="0" group="2" mask="0x00" low_byte="command1">NumberMsgHandler</message-handler>
<!-- handles all-link broadcast message ON -->
<message-handler cmd="0x11" ext="0" group="2" mask="0x01" low_byte="command1">NumberMsgHandler</message-handler>
<message-handler default="true">NoOpMsgHandler</message-handler>
<command-handler default="true">NoOpCommandHandler</command-handler>
<poll-handler>NoPollHandler</poll-handler> <!-- polled by ThermostatData1bGroup -->
</feature>
<feature name="VenstarIsCooling">
<message-dispatcher>DefaultDispatcher</message-dispatcher>
<!-- handles direct extended message after query -->
<message-handler cmd="0x2e" ext="1" match_cmd1="0x2e" match_cmd2="0x00" low_byte="userData8"
mask="0x01">NumberMsgHandler</message-handler>
<!-- handles all-link broadcast message OFF -->
<message-handler cmd="0x13" ext="0" group="1" mask="0x00" low_byte="command1">NumberMsgHandler</message-handler>
<!-- handles all-link broadcast message ON -->
<message-handler cmd="0x11" ext="0" group="1" mask="0x01" low_byte="command1">NumberMsgHandler</message-handler>
<message-handler default="true">NoOpMsgHandler</message-handler>
<command-handler default="true">NoOpCommandHandler</command-handler>
<poll-handler>NoPollHandler</poll-handler> <!-- polled by ThermostatData1bGroup -->
</feature>
<feature name="ReceiveBroadcast">
<message-dispatcher>DefaultDispatcher</message-dispatcher>
<!-- handles out-of band status messages -->
<message-handler cmd="0x11" ext="0" match_cmd1="0x11" low_byte="group">NumberMsgHandler</message-handler>
<message-handler default="true">NoOpMsgHandler</message-handler>
<poll-handler>NoPollHandler</poll-handler> <!-- broadcast messages only, no polling! -->
</feature>
<feature name="ExtStatusGroup"> <!-- does the polling for various quantities -->
<message-dispatcher>PollGroupDispatcher</message-dispatcher>
<poll-handler ext="2" cmd1="0x2e" cmd2="0x00" d1="0x01" d3="0x00">FlexPollHandler</poll-handler>
</feature>
<feature name="LEDBrightness">
<message-dispatcher>DefaultDispatcher</message-dispatcher>
<!-- handles direct extended message after query -->
<message-handler cmd="0x2e" ext="1" match_cmd1="0x2e" match_cmd2="0x00" match_d2="0x01"
low_byte="userData9">NumberMsgHandler</message-handler>
<!-- handles direct ack after poll -->
<message-handler cmd="0x19">TriggerPollMsgHandler</message-handler>
<message-handler default="true">NoOpMsgHandler</message-handler>
<command-handler command="DecimalType" ext="1" cmd1="0x2e" d1="0x01" d2="0x07" factor="1"
value="userData3">NumberCommandHandler</command-handler>
<command-handler default="true">NoOpCommandHandler</command-handler>
<poll-handler>NoPollHandler</poll-handler> <!-- polled by ExtStatusGroup -->
</feature>
<feature name="LEDOnOff">
<message-dispatcher>DefaultDispatcher</message-dispatcher>
<message-handler cmd="0x2F" mode="FAST">LightOnSwitchHandler</message-handler>
<message-handler cmd="0x2F" mode="FAST">LightOffSwitchHandler</message-handler>
<command-handler command="OnOffType">LEDOnOffCommandHandler</command-handler>
<poll-handler>NoPollHandler</poll-handler>
</feature>
<feature name="Beep">
<message-dispatcher>DefaultDispatcher</message-dispatcher>
<message-handler cmd="0x19">SwitchRequestReplyHandler</message-handler>
<command-handler command="OnOffType" off="0x30" on="0x30">RampOnOffCommandHandler</command-handler>
<poll-handler>NoPollHandler</poll-handler>
</feature>
<feature name="RampRate">
<message-dispatcher>DefaultDispatcher</message-dispatcher>
<!-- handles direct extended message after query -->
<message-handler cmd="0x2e" ext="1" match_cmd1="0x2e" match_cmd2="0x00" match_d2="0x01"
low_byte="userData7">NumberMsgHandler</message-handler>
<!-- handles direct ack after poll -->
<message-handler cmd="0x19">TriggerPollMsgHandler</message-handler>
<message-handler default="true">NoOpMsgHandler</message-handler>
<command-handler command="DecimalType" ext="1" cmd1="0x2e" d1="0x01" d2="0x05" factor="1"
value="userData3">NumberCommandHandler</command-handler>
<command-handler default="true">NoOpCommandHandler</command-handler>
<poll-handler>NoPollHandler</poll-handler> <!-- polled by ExtStatusGroup -->
</feature>
<feature name="OnLevel">
<message-dispatcher>DefaultDispatcher</message-dispatcher>
<!-- handles direct extended message after query -->
<message-handler cmd="0x2e" ext="1" match_cmd1="0x2e" match_cmd2="0x00" match_d2="0x01"
low_byte="userData8">NumberMsgHandler</message-handler>
<!-- handles direct ack after poll -->
<message-handler cmd="0x19">TriggerPollMsgHandler</message-handler>
<message-handler default="true">NoOpMsgHandler</message-handler>
<command-handler command="DecimalType" ext="1" cmd1="0x2e" d1="0x01" d2="0x06" factor="1"
value="userData3">NumberCommandHandler</command-handler>
<command-handler default="true">NoOpCommandHandler</command-handler>
<poll-handler>NoPollHandler</poll-handler> <!-- polled by ExtStatusGroup -->
</feature>
</xml>

View File

@@ -0,0 +1,598 @@
<xml>
<!-- device types
#
# PLEASE KEEP PRODUCT KEYS IN INCREASING ORDER:
#
# - first the devices with insteon assigned product keys
# - then X10 devices (key starting with X)
# - then Insteon devices with fake keys (starting with F)
#
#
Example entry:
<device productKey="F00.00.05">
<model>2456-D3</model>
<description>LampLinc V2</description>
<feature name="dimmer">GenericDimmer</feature>
<feature name="lastheardfrom">GenericLastTime</feature>
</device>
-->
<!-- #################################################
devices with regular insteon product keys
-->
<device productKey="0x00001A">
<model>2450</model>
<description>IO Link</description>
<feature name="contact">IOLincContact</feature>
<feature name="lastheardfrom">GenericLastTime</feature>
<feature name="switch">IOLincSwitch</feature>
</device>
<device productKey="0x000037">
<model>2486D</model>
<description>KeypadLinc Dimmer</description>
<feature name="dimmer">GenericDimmer</feature>
<feature name="manualchange">ManualChange</feature>
<feature name="fastonoff">FastOnOff</feature>
<feature name="lastheardfrom">GenericLastTime</feature>
</device>
<device productKey="0x000039">
<model>2663-222</model>
<description>On/Off Outlet</description>
<feature name="topoutlet">GenericSwitch</feature>
<feature name="bottomoutlet">BottomOutlet</feature>
<feature name="lastheardfrom">GenericLastTime</feature>
</device>
<device productKey="0x000041">
<model>2484DWH8</model>
<description>KeypadLinc Countdown Timer</description>
<feature name="dimmer">GenericDimmer</feature>
<feature name="manualchange">ManualChange</feature>
<feature name="fastonoff">FastOnOff</feature>
<feature name="lastheardfrom">GenericLastTime</feature>
</device>
<device productKey="0x000045">
<model>2413U</model>
<description>PowerLinc 2413U USB modem</description>
<feature name="broadcastonoff">GroupBroadcastOnOff</feature>
</device>
<device productKey="0x000049">
<model>2843-222</model>
<description>Wireless Open/Close Sensor</description>
<feature name="contact">GenericContact</feature>
<feature name="lastheardfrom">GenericLastTime</feature>
</device>
<device productKey="0x00004A">
<model>2842-222</model>
<description>Motion Sensor</description>
<feature name="contact">WirelessMotionSensorContact</feature>
<feature name="lightlevelabovethreshold">WirelessMotionSensorLightLevelAboveThreshold</feature>
<feature name="lowbattery">WirelessMotionSensorLowBattery</feature>
<feature name="data">MotionSensorData</feature>
<feature name="lastheardfrom">GenericLastTime</feature>
</device>
<device productKey="0x000050">
<model>2486DWH6</model>
<description>KeypadLinc Dimmer - 6 Button</description>
<feature name="loaddimmer">LoadDimmerButton</feature>
<feature name="rampdimmer">LoadDimmerRamp</feature>
<feature name="manualchange">ManualChange</feature>
<feature name="fastonoff">LoadDimmerFastOnOff</feature>
<feature_group name="button_group" type="KeyPadButtonGroup">
<feature name="keypadbuttona">KeyPadButton3</feature>
<feature name="keypadbuttonb">KeyPadButton4</feature>
<feature name="keypadbuttonc">KeyPadButton5</feature>
<feature name="keypadbuttond">KeyPadButton6</feature>
</feature_group>
<feature name="lastheardfrom">GenericLastTime</feature>
</device>
<device productKey="0x000051">
<model>2486DWH8</model>
<description>KeypadLinc Dimmer - 8 Button</description>
<feature name="loaddimmer">LoadDimmerButton</feature>
<feature name="rampdimmer">LoadDimmerRamp</feature>
<feature name="manualchange">ManualChange</feature>
<feature name="fastonoff">LoadDimmerFastOnOff</feature>
<feature_group name="button_group" type="KeyPadButtonGroup">
<feature name="keypadbuttonb">KeyPadButton2</feature>
<feature name="keypadbuttonc">KeyPadButton3</feature>
<feature name="keypadbuttond">KeyPadButton4</feature>
<feature name="keypadbuttone">KeyPadButton5</feature>
<feature name="keypadbuttonf">KeyPadButton6</feature>
<feature name="keypadbuttong">KeyPadButton7</feature>
<feature name="keypadbuttonh">KeyPadButton8</feature>
</feature_group>
<feature name="lastheardfrom">GenericLastTime</feature>
</device>
<device productKey="0x000068">
<model>2472D</model>
<description>OutletLinc Dimmer</description>
<feature name="dimmer">GenericDimmer</feature>
<feature name="manualchange">ManualChange</feature>
<feature name="fastonoff">FastOnOff</feature>
<feature name="lastheardfrom">GenericLastTime</feature>
<feature name="ledonoff">LEDOnOff</feature>
<feature name="beep">Beep</feature>
<feature_group name="ext_group" type="ExtStatusGroup">
<feature name="ledbrightness">LEDBrightness</feature>
<feature name="ramprate">RampRate</feature>
</feature_group>
</device>
<!-- #################################################
X10 devices with made-up product keys Xaa.bb.cc
-->
<device productKey="X00.00.01">
<model>X10 switch</model>
<description>any simple X10 switch</description>
<feature name="switch">X10Switch</feature>
</device>
<device productKey="X00.00.02">
<model>X10 dimmer</model>
<description>Generic X10 Dimmer without preset</description>
<feature name="switch">X10Switch</feature>
<feature name="dimmer">X10Dimmer</feature>
</device>
<device productKey="X00.00.03">
<model>X10 motion sensor</model>
<description>Generic X10 motion sensor</description>
<feature name="contact">X10Contact</feature>
</device>
<!-- ###################################################
Insteon devices with made-up product keys Faa.bb.cc
-->
<device productKey="F00.00.01">
<model>2477D</model>
<description>SwitchLinc Dimmer</description>
<feature name="dimmer">GenericDimmer</feature>
<feature name="manualchange">ManualChange</feature>
<feature name="fastonoff">FastOnOff</feature>
<feature name="lastheardfrom">GenericLastTime</feature>
<feature name="ledonoff">LEDOnOff</feature>
<feature name="beep">Beep</feature>
<feature_group name="ext_group" type="ExtStatusGroup">
<feature name="ledbrightness">LEDBrightness</feature>
<feature name="ramprate">RampRate</feature>
<feature name="onlevel">OnLevel</feature>
</feature_group>
</device>
<device productKey="F00.00.02">
<model>2477S</model>
<description>SwitchLinc Switch</description>
<feature name="switch">GenericSwitch</feature>
<feature name="fastonoff">FastOnOff</feature>
<feature name="lastheardfrom">GenericLastTime</feature>
<feature name="ledonoff">LEDOnOff</feature>
<feature name="beep">Beep</feature>
<feature_group name="ext_group" type="ExtStatusGroup">
<feature name="ledbrightness">LEDBrightness</feature>
</feature_group>
</device>
<device productKey="F00.00.03">
<model>2845-222</model>
<description>Hidden Door Sensor</description>
<feature name="contact">WirelessMotionSensorContact</feature>
<feature name="data">HiddenDoorSensorData</feature>
<feature name="lastheardfrom">GenericLastTime</feature>
</device>
<device productKey="F00.00.04">
<model>2876S</model>
<description>ICON Switch</description>
<feature name="switch">GenericSwitch</feature>
<feature name="lastheardfrom">GenericLastTime</feature>
</device>
<device productKey="F00.00.05">
<model>2456D3</model>
<description>LampLinc V2</description>
<feature name="dimmer">GenericDimmer</feature>
<feature name="manualchange">ManualChange</feature>
<feature name="fastonoff">FastOnOff</feature>
<feature name="lastheardfrom">GenericLastTime</feature>
</device>
<device productKey="F00.00.06">
<model>2442-222</model>
<description>Micro Dimmer</description>
<feature name="dimmer">GenericDimmer</feature>
<feature name="manualchange">ManualChange</feature>
<feature name="fastonoff">FastOnOff</feature>
<feature name="lastheardfrom">GenericLastTime</feature>
</device>
<device productKey="F00.00.07">
<model>2453-222</model>
<description>DIN Rail On/Off</description>
<feature name="switch">GenericSwitch</feature>
<feature name="lastheardfrom">GenericLastTime</feature>
</device>
<device productKey="F00.00.08">
<model>2452-222</model>
<description>DIN Rail Dimmer</description>
<feature name="dimmer">GenericDimmer</feature>
<feature name="manualchange">ManualChange</feature>
<feature name="fastonoff">FastOnOff</feature>
<feature name="lastheardfrom">GenericLastTime</feature>
</device>
<device productKey="F00.00.09">
<model>2458-A1</model>
<description>MorningLinc RF Lock Controller</description>
<feature name="switch">GenericSwitch</feature>
</device>
<device productKey="F00.00.0A">
<model>2852-222</model>
<description>Leak Sensor</description>
<feature name="contact">LeakSensorContact</feature>
<feature name="lastheardfrom">GenericLastTime</feature>
</device>
<device productKey="F00.00.0B">
<model>2672-422</model>
<description>LED Dimmer</description>
<feature name="dimmer">GenericDimmer</feature>
<feature name="manualchange">ManualChange</feature>
<feature name="fastonoff">FastOnOff</feature>
<feature name="lastheardfrom">GenericLastTime</feature>
</device>
<device productKey="F00.00.0C">
<model>2476D</model>
<description>SwitchLinc Dimmer</description>
<feature name="dimmer">GenericDimmer</feature>
<feature name="manualchange">ManualChange</feature>
<feature name="fastonoff">FastOnOff</feature>
<feature name="lastheardfrom">GenericLastTime</feature>
</device>
<device productKey="F00.00.0D">
<model>2634-222</model>
<description>On/Off Dual-Band Outdoor Module</description>
<feature name="switch">GenericSwitch</feature>
<feature name="lastheardfrom">GenericLastTime</feature>
<feature name="ledonoff">LEDOnOff</feature>
<feature name="beep">Beep</feature>
</device>
<device productKey="F00.00.10">
<model>2342-2</model>
<description>Mini Remote</description>
<feature name="buttona">RemoteButton1</feature>
<feature name="buttonb">RemoteButton2</feature>
<feature name="buttonc">RemoteButton3</feature>
<feature name="buttond">RemoteButton4</feature>
<feature name="lastheardfrom">GenericLastTime</feature>
</device>
<device productKey="F00.00.11">
<model>2466D</model>
<description>ToggleLinc Dimmer</description>
<feature name="dimmer">GenericDimmer</feature>
<feature name="rampdimmer">RampDimmer</feature>
<feature name="manualchange">ManualChange</feature>
<feature name="fastonoff">FastOnOff</feature>
<feature name="lastheardfrom">GenericLastTime</feature>
</device>
<device productKey="F00.00.12">
<model>2466S</model>
<description>ToggleLinc Switch</description>
<feature name="switch">GenericSwitch</feature>
<feature name="lastheardfrom">GenericLastTime</feature>
</device>
<device productKey="F00.00.13">
<model>2672-222</model>
<description>LED Bulb</description>
<feature name="dimmer">GenericDimmer</feature>
<feature name="rampdimmer">RampDimmer_3435</feature>
<feature name="manualchange">ManualChange</feature>
<feature name="fastonoff">FastOnOff</feature>
<feature name="lastheardfrom">GenericLastTime</feature>
</device>
<device productKey="F00.00.14">
<model>2487S</model>
<description>KeypadLinc On/Off 6-Button Scene Control </description>
<feature name="loadswitch">LoadSwitchButton</feature>
<feature name="loadswitchmanualchange">LoadSwitchManualChange</feature>
<feature name="loadswitchfastonoff">LoadSwitchFastOnOff</feature>
<feature_group name="button_group" type="KeyPadButtonGroup">
<feature name="keypadbuttona">KeyPadButton3</feature>
<feature name="keypadbuttonb">KeyPadButton4</feature>
<feature name="keypadbuttonc">KeyPadButton5</feature>
<feature name="keypadbuttond">KeyPadButton6</feature>
</feature_group>
<feature_group name="fastonoff_button_group" type="FastOnOffButtonGroup">
<feature name="fastonoffbuttona">FastOnOffButton3</feature>
<feature name="fastonoffbuttonb">FastOnOffButton4</feature>
<feature name="fastonoffbuttonc">FastOnOffButton5</feature>
<feature name="fastonoffbuttond">FastOnOffButton6</feature>
</feature_group>
<feature_group name="manualchange_button_group" type="ManualChangeButtonGroup">
<feature name="manualchangebuttona">ManualChangeButton3</feature>
<feature name="manualchangebuttonb">ManualChangeButton4</feature>
<feature name="manualchangebuttonc">ManualChangeButton5</feature>
<feature name="manualchangebuttond">ManualChangeButton6</feature>
</feature_group>
<feature name="lastheardfrom">GenericLastTime</feature>
</device>
<device productKey="F00.00.15">
<model>2334-232</model>
<description>Keypad Dimmer Switch, 6-Button </description>
<feature name="loaddimmer">LoadDimmerButton</feature>
<feature name="rampdimmer">LoadDimmerRamp</feature>
<feature name="manualchange">ManualChange</feature>
<feature name="fastonoff">LoadDimmerFastOnOff</feature>
<feature_group name="button_group" type="KeyPadButtonGroup">
<feature name="keypadbuttona">KeyPadButton3</feature>
<feature name="keypadbuttonb">KeyPadButton4</feature>
<feature name="keypadbuttonc">KeyPadButton5</feature>
<feature name="keypadbuttond">KeyPadButton6</feature>
</feature_group>
<feature_group name="fastonoff_button_group" type="FastOnOffButtonGroup">
<feature name="fastonoffbuttona">FastOnOffButton3</feature>
<feature name="fastonoffbuttonb">FastOnOffButton4</feature>
<feature name="fastonoffbuttonc">FastOnOffButton5</feature>
<feature name="fastonoffbuttond">FastOnOffButton6</feature>
</feature_group>
<feature_group name="manualchange_button_group" type="ManualChangeButtonGroup">
<feature name="manualchangebuttona">ManualChangeButton3</feature>
<feature name="manualchangebuttonb">ManualChangeButton4</feature>
<feature name="manualchangebuttonc">ManualChangeButton5</feature>
<feature name="manualchangebuttond">ManualChangeButton6</feature>
</feature_group>
<feature_group name="ext_group" type="ExtStatusGroup">
<feature name="ledbrightness">LEDBrightness</feature>
<feature name="ramprate">RampRate</feature>
<feature name="onlevel">OnLevel</feature>
</feature_group>
<feature name="ledonoff">LEDOnOff</feature>
<feature name="beep">Beep</feature>
<feature name="lastheardfrom">GenericLastTime</feature>
</device>
<device productKey="F00.00.16">
<model>2334-232</model>
<description>Keypad Dimmer Switch, 8-Button </description>
<feature name="loaddimmer">LoadDimmerButton</feature>
<feature name="rampdimmer">LoadDimmerRamp</feature>
<feature name="manualchange">ManualChange</feature>
<feature name="fastonoff">LoadDimmerFastOnOff</feature>
<feature_group name="button_group" type="KeyPadButtonGroup">
<feature name="keypadbuttonb">KeyPadButton2</feature>
<feature name="keypadbuttonc">KeyPadButton3</feature>
<feature name="keypadbuttond">KeyPadButton4</feature>
<feature name="keypadbuttone">KeyPadButton5</feature>
<feature name="keypadbuttonf">KeyPadButton6</feature>
<feature name="keypadbuttong">KeyPadButton7</feature>
<feature name="keypadbuttonh">KeyPadButton8</feature>
</feature_group>
<feature_group name="fastonoff_button_group" type="FastOnOffButtonGroup">
<feature name="fastonoffbuttonb">FastOnOffButton2</feature>
<feature name="fastonoffbuttonc">FastOnOffButton3</feature>
<feature name="fastonoffbuttond">FastOnOffButton4</feature>
<feature name="fastonoffbuttone">FastOnOffButton5</feature>
<feature name="fastonoffbuttonf">FastOnOffButton6</feature>
<feature name="fastonoffbuttong">FastOnOffButton7</feature>
<feature name="fastonoffbuttonh">FastOnOffButton8</feature>
</feature_group>
<feature_group name="manualchange_button_group" type="ManualChangeButtonGroup">
<feature name="manualchangebuttonb">ManualChangeButton2</feature>
<feature name="manualchangebuttonc">ManualChangeButton3</feature>
<feature name="manualchangebuttond">ManualChangeButton4</feature>
<feature name="manualchangebuttone">ManualChangeButton5</feature>
<feature name="manualchangebuttonf">ManualChangeButton6</feature>
<feature name="manualchangebuttong">ManualChangeButton7</feature>
<feature name="manualchangebuttonh">ManualChangeButton8</feature>
</feature_group>
<feature_group name="ext_group" type="ExtStatusGroup">
<feature name="ledbrightness">LEDBrightness</feature>
<feature name="ramprate">RampRate</feature>
<feature name="onlevel">OnLevel</feature>
</feature_group>
<feature name="ledonoff">LEDOnOff</feature>
<feature name="beep">Beep</feature>
<feature name="lastheardfrom">GenericLastTime</feature>
</device>
<device productKey="F00.00.17">
<model>2423A1</model>
<description>iMeter Solo Power Meter</description>
<feature name="meter">PowerMeter</feature>
<feature name="lastheardfrom">GenericLastTime</feature>
</device>
<device productKey="F00.00.18">
<model>2441TH</model>
<description>Insteon Thermostat</description>
<feature_group name="data1_group" type="ThermostatData1Group">
<feature name="backlightduration">ThermostatBackLightDuration</feature>
<feature name="acdelay">ThermostatACDelay</feature>
</feature_group>
<feature_group name="data1b_group" type="ThermostatData1bGroup">
<feature name="humidityhigh">ThermostatHumidityHigh</feature>
<feature name="humiditylow">ThermostatHumidityLow</feature>
<feature name="stage1duration">ThermostatStage1Duration</feature>
</feature_group>
<feature_group name="data2_group" type="ThermostatData2Group">
<feature name="coolsetpoint">ThermostatCoolSetPoint</feature>
<feature name="heatsetpoint">ThermostatHeatSetPoint</feature>
<feature name="systemmode">ThermostatSystemMode</feature>
<feature name="fanmode">ThermostatFanMode</feature>
<feature name="isheating">ThermostatIsHeating</feature>
<feature name="iscooling">ThermostatIsCooling</feature>
<feature name="temperature">ThermostatTemperatureFahrenheit</feature>
<!--
<feature name="tempcelsius">ThermostatTemperatureCelsius</feature>
<feature name="tempfahrenheit">ThermostatTemperatureFahrenheit</feature>
-->
<feature name="humidity">ThermostatHumidity</feature>
</feature_group>
<feature name="lastheardfrom">GenericLastTime</feature>
</device>
<device productKey="F00.00.19">
<model>2457D2</model>
<description>LampLinc Dimmer</description>
<feature name="dimmer">GenericDimmer</feature>
<feature name="manualchange">ManualChange</feature>
<feature name="fastonoff">FastOnOff</feature>
<feature name="lastheardfrom">GenericLastTime</feature>
<feature name="ledonoff">LEDOnOff</feature>
<feature name="beep">Beep</feature>
</device>
<device productKey="F00.00.1A">
<model>2475SDB</model>
<description>In-LineLinc Relay</description>
<feature name="switch">GenericSwitch</feature>
<feature name="lastheardfrom">GenericLastTime</feature>
</device>
<device productKey="F00.00.1B">
<model>2635-222</model>
<description>On/Off Module</description>
<feature name="switch">GenericSwitch</feature>
<feature name="lastheardfrom">GenericLastTime</feature>
<feature name="ledonoff">LEDOnOff</feature>
<feature name="beep">Beep</feature>
</device>
<device productKey="F00.00.1C">
<model>2475F</model>
<description>FanLinc Module</description>
<feature name="lightdimmer">GenericDimmer</feature>
<feature name="fan">FanLincFan</feature>
<feature name="lastheardfrom">GenericLastTime</feature>
</device>
<device productKey="F00.00.1D">
<model>2456S3</model>
<description>ApplianceLinc</description>
<feature name="switch">GenericSwitch</feature>
<feature name="lastheardfrom">GenericLastTime</feature>
</device>
<device productKey="F00.00.1E">
<model>2674-222</model>
<description>LED Bulb (recessed)</description>
<feature name="dimmer">GenericDimmer</feature>
<feature name="rampdimmer">RampDimmer</feature>
<feature name="manualchange">ManualChange</feature>
<feature name="fastonoff">FastOnOff</feature>
<feature name="lastheardfrom">GenericLastTime</feature>
</device>
<device productKey="F00.00.1F">
<model>2477SA1</model>
<description>220V 30-amp Load Controller N/O</description>
<feature name="switch">GenericSwitch</feature>
<feature name="fastonoff">FastOnOff</feature>
<feature name="lastheardfrom">GenericLastTime</feature>
</device>
<device productKey="F00.00.20">
<model>2342-222</model>
<description>Mini Remote (8 Button)</description>
<feature name="buttona">RemoteButton2</feature>
<feature name="buttonb">RemoteButton1</feature>
<feature name="buttonc">RemoteButton4</feature>
<feature name="buttond">RemoteButton3</feature>
<feature name="buttone">RemoteButton6</feature>
<feature name="buttonf">RemoteButton5</feature>
<feature name="buttong">RemoteButton8</feature>
<feature name="buttonh">RemoteButton7</feature>
<feature name="lastheardfrom">GenericLastTime</feature>
</device>
<device productKey="F00.00.21">
<model>2441V</model>
<description>Insteon Thermostat Adaptor for Venstar</description>
<feature_group name="data1b_group" type="ThermostatData1bGroup">
<feature name="coolsetpoint">VenstarCoolSetPoint</feature>
<feature name="heatsetpoint">VenstarHeatSetPoint</feature>
<feature name="systemmode">VenstarSystemMode</feature>
<feature name="fanmode">VenstarFanMode</feature>
<feature name="tempfahrenheit">VenstarTemperatureFahrenheit</feature>
<feature name="humidity">VenstarHumidity</feature>
<feature name="isheating">VenstarIsHeating</feature>
<feature name="iscooling">VenstarIsCooling</feature>
</feature_group>
<feature name="lastheardfrom">GenericLastTime</feature>
</device>
<device productKey="F00.00.22">
<model>2982-222</model>
<description>Insteon Smoke Bridge</description>
<feature name="notification">ReceiveBroadcast</feature>
<feature name="lastheardfrom">GenericLastTime</feature>
</device>
<device productKey="F00.00.23">
<model>2487S</model>
<description>KeypadLinc On/Off 8-Button Scene Control </description>
<feature name="loadswitch">LoadSwitchButton</feature>
<feature name="loadswitchmanualchange">LoadSwitchManualChange</feature>
<feature name="loadswitchfastonoff">LoadSwitchFastOnOff</feature>
<feature_group name="button_group" type="KeyPadButtonGroup">
<feature name="keypadbuttonb">KeyPadButton2</feature>
<feature name="keypadbuttonc">KeyPadButton3</feature>
<feature name="keypadbuttond">KeyPadButton4</feature>
<feature name="keypadbuttone">KeyPadButton5</feature>
<feature name="keypadbuttonf">KeyPadButton6</feature>
<feature name="keypadbuttong">KeyPadButton7</feature>
<feature name="keypadbuttonh">KeyPadButton8</feature>
</feature_group>
<feature_group name="fastonoff_button_group" type="FastOnOffButtonGroup">
<feature name="fastonoffbuttonb">FastOnOffButton2</feature>
<feature name="fastonoffbuttonc">FastOnOffButton3</feature>
<feature name="fastonoffbuttond">FastOnOffButton4</feature>
<feature name="fastonoffbuttone">FastOnOffButton5</feature>
<feature name="fastonoffbuttonf">FastOnOffButton6</feature>
<feature name="fastonoffbuttong">FastOnOffButton7</feature>
<feature name="fastonoffbuttonh">FastOnOffButton8</feature>
</feature_group>
<feature_group name="manualchange_button_group" type="ManualChangeButtonGroup">
<feature name="manualchangebuttonb">ManualChangeButton2</feature>
<feature name="manualchangebuttonc">ManualChangeButton3</feature>
<feature name="manualchangebuttond">ManualChangeButton4</feature>
<feature name="manualchangebuttone">ManualChangeButton5</feature>
<feature name="manualchangebuttonf">ManualChangeButton6</feature>
<feature name="manualchangebuttong">ManualChangeButton7</feature>
<feature name="manualchangebuttonh">ManualChangeButton8</feature>
</feature_group>
<feature name="lastheardfrom">GenericLastTime</feature>
</device>
<device productKey="F00.00.24">
<model>2844-222</model>
<description>Motion Sensor II</description>
<feature name="contact">WirelessMotionSensorContact</feature>
<feature name="lightlevelabovethreshold">WirelessMotionSensorLightLevelAboveThreshold</feature>
<feature name="lowbattery">WirelessMotionSensorLowBattery</feature>
<feature name="data">MotionSensor2Data</feature>
<feature name="tamperswitch">WirelessMotionSensor2TamperSwitch</feature>
<feature name="lastheardfrom">GenericLastTime</feature>
</device>
</xml>

View File

@@ -0,0 +1,410 @@
<xml>
<!--
// Please keep messages ordered by increasing command number!
//
// The header is not an official Insteon concept. The boundary
// between header and message is the point where enough information
// is available to determine the full length of the incoming message.
// Sometimes that's just two bytes, sometimes 6 or 9.
//
->
<!-
// The PureNACK message is a fake message that was introduced
// to make the driver code more regular. It indicates
// that the modem was not ready, and that the host has to
// write the message to the serial port again.
-->
<msg name="PureNACK" length="2" direction="FROM_MODEM">
<header length="2">
<byte>0x02</byte>
<byte>0x15</byte>
</header>
</msg>
<msg name="StandardMessageReceived" length="11" direction="FROM_MODEM">
<header length="9">
<byte>0x02</byte>
<byte name="Cmd">0x50</byte>
<address name="fromAddress"/>
<address name="toAddress"/>
<byte name="messageFlags"/>
</header>
<byte name="command1"/>
<byte name="command2"/>
</msg>
<msg name="ExtendedMessageReceived" length="25" direction="FROM_MODEM">
<header length="9">
<byte>0x02</byte>
<byte name="Cmd">0x51</byte>
<address name="fromAddress"/>
<address name="toAddress"/>
<byte name="messageFlags">0x10</byte>
</header>
<byte name="command1"/>
<byte name="command2"/>
<byte name="userData1"/>
<byte name="userData2"/>
<byte name="userData3"/>
<byte name="userData4"/>
<byte name="userData5"/>
<byte name="userData6"/>
<byte name="userData7"/>
<byte name="userData8"/>
<byte name="userData9"/>
<byte name="userData10"/>
<byte name="userData11"/>
<byte name="userData12"/>
<byte name="userData13"/>
<byte name="userData14"/>
</msg>
<msg name="X10Received" length="4" direction="FROM_MODEM">
<header length="2">
<byte>0x02</byte>
<byte name="Cmd">0x52</byte>
</header>
<byte name="rawX10"/>
<byte name="X10Flag"/>
</msg>
<msg name="ALLLinkingCompleted" length="10" direction="FROM_MODEM">
<header length="2">
<byte>0x02</byte>
<byte name="Cmd">0x53</byte>
</header>
<byte name="linkCode"/>
<byte name="ALLLinkGroup"/>
<address name="address"/>
<byte name="deviceCategory"/>
<byte name="subCategory"/>
<byte name="firmwareVersion"/>
</msg>
<msg name="ButtonEventReport" length="3" direction="FROM_MODEM">
<header length="2">
<byte>0x02</byte>
<byte name="Cmd">0x54</byte>
</header>
<byte name="buttonEvent"/>
</msg>
<msg name="UserResetDetected" length="2" direction="FROM_MODEM">
<header length="2">
<byte>0x02</byte>
<byte name="Cmd">0x55</byte>
</header>
</msg>
<msg name="ALLLinkCleanupFailureReport" length="6" direction="FROM_MODEM">
<header length="2">
<byte>0x02</byte>
<byte name="Cmd">0x56</byte>
</header>
<byte name="ALLLinkGroup"/>
<address name="address"/>
</msg>
<msg name="ALLLinkRecordResponse" length="10" direction="FROM_MODEM">
<header length="2">
<byte>0x02</byte>
<byte name="Cmd">0x57</byte>
</header>
<byte name="RecordFlags"/>
<byte name="ALLLinkGroup"/>
<address name="LinkAddr"/>
<byte name="LinkData1"/>
<byte name="LinkData2"/>
<byte name="LinkData3"/>
</msg>
<msg name="ALLLinkCleanupStatusReport" length="3" direction="FROM_MODEM">
<header length="2">
<byte>0x02</byte>
<byte name="Cmd">0x58</byte>
</header>
<byte name="statusByte"/>
</msg>
<msg name="UnknownMessage5C" length="11" direction="FROM_MODEM">
<header length="9">
<byte>0x02</byte>
<byte name="Cmd">0x5c</byte>
<address name="fromAddress"/>
<address name="toAddress"/>
<byte name="messageFlags"/>
</header>
<byte name="command1"/>
<byte name="command2"/>
</msg>
<msg name="GetIMInfo" length="2" direction="TO_MODEM">
<header length="2">
<byte>0x02</byte>
<byte name="Cmd">0x60</byte>
</header>
</msg>
<msg name="GetIMInfoReply" length="9" direction="FROM_MODEM">
<header length="2">
<byte>0x02</byte>
<byte name="Cmd">0x60</byte>
</header>
<address name="IMAddress"/>
<byte name="DeviceCategory"/>
<byte name="DeviceSubCategory"/>
<byte name="FirmwareVersion"/>
<byte name="ACK/NACK"/>
</msg>
<msg name="SendALLLinkCommand" length="5" direction="TO_MODEM">
<header length="2">
<byte>0x02</byte>
<byte name="Cmd">0x61</byte>
</header>
<byte name="ALLLinkGroup">0x00</byte>
<byte name="ALLLinkCommand">0x00</byte>
<byte name="BroadcastCommand2">0x00</byte>
</msg>
<msg name="SendALLLinkCommandReply" length="6" direction="FROM_MODEM">
<header length="2">
<byte>0x02</byte>
<byte name="Cmd">0x61</byte>
</header>
<byte name="ALLLinkGroup">0x00</byte>
<byte name="ALLLinkCommand">0x00</byte>
<byte name="BroadcastCommand2">0x00</byte>
<byte name="ACK/NACK"/>
</msg>
<msg name="SendStandardMessage" length="8" direction="TO_MODEM">
<header length="6">
<byte>0x02</byte>
<byte name="Cmd">0x62</byte>
<address name="toAddress"/>
<byte name="messageFlags"/>
</header>
<byte name="command1"/>
<byte name="command2"/>
</msg>
<msg name="SendStandardMessageReply" length="9" direction="FROM_MODEM">
<header length="6">
<byte>0x02</byte>
<byte name="Cmd">0x62</byte>
<address name="toAddress"/>
<byte name="messageFlags"/>
</header>
<byte name="command1"/>
<byte name="command2"/>
<byte name="ACK/NACK"/>
</msg>
<msg name="SendExtendedMessage" length="22" direction="TO_MODEM">
<header length="6">
<byte>0x02</byte>
<byte name="Cmd">0x62</byte>
<address name="toAddress"/>
<byte name="messageFlags">0x10</byte>
</header>
<byte name="command1">0x00</byte>
<byte name="command2">0x00</byte>
<byte name="userData1">0x00</byte>
<byte name="userData2">0x00</byte>
<byte name="userData3">0x00</byte>
<byte name="userData4">0x00</byte>
<byte name="userData5">0x00</byte>
<byte name="userData6">0x00</byte>
<byte name="userData7">0x00</byte>
<byte name="userData8">0x00</byte>
<byte name="userData9">0x00</byte>
<byte name="userData10">0x00</byte>
<byte name="userData11">0x00</byte>
<byte name="userData12">0x00</byte>
<byte name="userData13">0x00</byte>
<byte name="userData14">0x00</byte>
</msg>
<msg name="SendExtendedMessageReply" length="23" direction="FROM_MODEM">
<header length="6">
<byte>0x02</byte>
<byte name="Cmd">0x62</byte>
<address name="toAddress"/>
<byte name="messageFlags">0x10</byte>
</header>
<byte name="command1"/>
<byte name="command2"/>
<byte name="userData1"/>
<byte name="userData2"/>
<byte name="userData3"/>
<byte name="userData4"/>
<byte name="userData5"/>
<byte name="userData6"/>
<byte name="userData7"/>
<byte name="userData8"/>
<byte name="userData9"/>
<byte name="userData10"/>
<byte name="userData11"/>
<byte name="userData12"/>
<byte name="userData13"/>
<byte name="userData14"/>
<byte name="ACK/NACK"/>
</msg>
<msg name="SendX10Message" length="4" direction="TO_MODEM">
<header length="2">
<byte>0x02</byte>
<byte name="Cmd">0x63</byte>
</header>
<byte name="rawX10"></byte>
<byte name="X10Flag">0x00</byte>
</msg>
<msg name="SendX10MessageReply" length="5" direction="FROM_MODEM">
<header length="2">
<byte>0x02</byte>
<byte name="Cmd">0x63</byte>
</header>
<byte name="rawX10"></byte>
<byte name="X10Flag">0x00</byte>
<byte name="ACK/NACK"/>
</msg>
<msg name="StartALLLinking" length="4" direction="TO_MODEM">
<header length="2">
<byte>0x02</byte>
<byte name="Cmd">0x64</byte>
</header>
<byte name="LinkCode"></byte>
<byte name="ALLLinkGroup">0x00</byte>
</msg>
<msg name="StartALLLinkingReply" length="5" direction="FROM_MODEM">
<header length="2">
<byte>0x02</byte>
<byte name="Cmd">0x64</byte>
</header>
<byte name="LinkCode"></byte>
<byte name="ALLLinkGroup">0x00</byte>
<byte name="ACK/NACK"/>
</msg>
<msg name="CancelALLLinking" length="2" direction="TO_MODEM">
<header length="2">
<byte>0x02</byte>
<byte name="Cmd">0x65</byte>
</header>
</msg>
<msg name="CancelALLLinkingReply" length="3" direction="FROM_MODEM">
<header length="2">
<byte>0x02</byte>
<byte name="Cmd">0x65</byte>
</header>
<byte name="ACK/NACK"/>
</msg>
<msg name="SetHostDeviceCategory" length="5" direction="TO_MODEM">
<header length="2">
<byte>0x02</byte>
<byte name="Cmd">0x66</byte>
</header>
<byte name="DeviceCategory"/>
<byte name="DeviceSubcategory"/>
<byte name="FirmwareVersion"/>
</msg>
<msg name="SetHostDeviceCategoryReply" length="6" direction="FROM_MODEM">
<header length="2">
<byte>0x02</byte>
<byte name="Cmd">0x66</byte>
</header>
<byte name="DeviceCategory"/>
<byte name="DeviceSubcategory"/>
<byte name="FirmwareVersion"/>
<byte name="ACK/NACK"/>
</msg>
<msg name="ResetIM" length="2" direction="TO_MODEM">
<header length="2">
<byte>0x02</byte>
<byte name="Cmd">0x67</byte>
</header>
</msg>
<msg name="ResetIMReply" length="3" direction="FROM_MODEM">
<header length="2">
<byte>0x02</byte>
<byte name="Cmd">0x67</byte>
</header>
<byte name="ACK/NACK"/>
</msg>
<msg name="SetAckMessageByte" length="3" direction="TO_MODEM">
<header length="2">
<byte>0x02</byte>
<byte name="Cmd">0x68</byte>
</header>
<byte name="Command2"/>
</msg>
<msg name="SetAckMessageByteReply" length="4" direction="FROM_MODEM">
<header length="2">
<byte>0x02</byte>
<byte name="Cmd">0x68</byte>
</header>
<byte name="Command2"/>
<byte name="ACK/NACK"/>
</msg>
<msg name="GetFirstALLLinkRecord" length="2" direction="TO_MODEM">
<header length="2">
<byte>0x02</byte>
<byte name="Cmd">0x69</byte>
</header>
</msg>
<msg name="GetFirstALLLinkRecordReply" length="3" direction="FROM_MODEM">
<header length="2">
<byte>0x02</byte>
<byte name="Cmd">0x69</byte>
</header>
<byte name="ACK/NACK"/>
</msg>
<msg name="GetNextALLLinkRecord" length="2" direction="TO_MODEM">
<header length="2">
<byte>0x02</byte>
<byte name="Cmd">0x6a</byte>
</header>
</msg>
<msg name="GetNextALLLinkRecordReply" length="3" direction="FROM_MODEM">
<header length="2">
<byte>0x02</byte>
<byte name="Cmd">0x6a</byte>
</header>
<byte name="ACK/NACK"/>
</msg>
<msg name="ManageALLLinkRecord" length="11" direction="TO_MODEM">
<header length="2">
<byte>0x02</byte>
<byte name="Cmd">0x6f</byte>
</header>
<byte name="controlCode"/>
<byte name="recordFlags"/>
<byte name="ALLLinkGroup"/>
<address name="linkAddress"/>
<byte name="linkData1"/>
<byte name="linkData2"/>
<byte name="linkData3"/>
</msg>
<msg name="ManageALLLinkRecordReply" length="12" direction="FROM_MODEM">
<header length="2">
<byte>0x02</byte>
<byte name="Cmd">0x6f</byte>
</header>
<byte name="controlCode"/>
<byte name="recordFlags"/>
<byte name="ALLLinkGroup"/>
<address name="linkAddress"/>
<byte name="linkData1"/>
<byte name="linkData2"/>
<byte name="linkData3"/>
<byte name="ACK/NACK"/>
</msg>
<msg name="Beep" length="2" direction="TO_MODEM">
<header length="2">
<byte>0x02</byte>
<byte name="Cmd">0x77</byte>
</header>
</msg>
<msg name="BeepReply" length="3" direction="FROM_MODEM">
<header length="2">
<byte>0x02</byte>
<byte name="Cmd">0x77</byte>
</header>
<byte name="ACK/NACK"/>
</msg>
<msg name="UnknownMessage7F" length="4" direction="FROM_MODEM">
<header length="2">
<byte>0x02</byte>
<byte name="Cmd">0x7F</byte>
</header>
<byte name="Command2"/>
<byte name="ACK/NACK"/>
</msg>
</xml>