added migrated 2.x add-ons
Signed-off-by: Kai Kreuzer <kai@openhab.org>
This commit is contained in:
@@ -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>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
@@ -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<>()));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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(",$", ""));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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() + ")";
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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+@", ":******@");
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user