2 * Copyright (c) 2010-2024 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.insteon.internal;
15 import java.io.IOException;
16 import java.util.ArrayList;
17 import java.util.Collections;
18 import java.util.Comparator;
19 import java.util.HashMap;
20 import java.util.HashSet;
21 import java.util.Iterator;
22 import java.util.List;
24 import java.util.Map.Entry;
26 import java.util.concurrent.ConcurrentHashMap;
27 import java.util.concurrent.ScheduledExecutorService;
29 import javax.xml.parsers.ParserConfigurationException;
31 import org.eclipse.jdt.annotation.NonNullByDefault;
32 import org.eclipse.jdt.annotation.Nullable;
33 import org.openhab.binding.insteon.internal.config.InsteonChannelConfiguration;
34 import org.openhab.binding.insteon.internal.config.InsteonNetworkConfiguration;
35 import org.openhab.binding.insteon.internal.device.DeviceFeature;
36 import org.openhab.binding.insteon.internal.device.DeviceFeatureListener;
37 import org.openhab.binding.insteon.internal.device.DeviceType;
38 import org.openhab.binding.insteon.internal.device.DeviceTypeLoader;
39 import org.openhab.binding.insteon.internal.device.InsteonAddress;
40 import org.openhab.binding.insteon.internal.device.InsteonDevice;
41 import org.openhab.binding.insteon.internal.device.InsteonDevice.DeviceStatus;
42 import org.openhab.binding.insteon.internal.device.RequestQueueManager;
43 import org.openhab.binding.insteon.internal.driver.Driver;
44 import org.openhab.binding.insteon.internal.driver.DriverListener;
45 import org.openhab.binding.insteon.internal.driver.ModemDBEntry;
46 import org.openhab.binding.insteon.internal.driver.Poller;
47 import org.openhab.binding.insteon.internal.driver.Port;
48 import org.openhab.binding.insteon.internal.handler.InsteonDeviceHandler;
49 import org.openhab.binding.insteon.internal.handler.InsteonNetworkHandler;
50 import org.openhab.binding.insteon.internal.message.FieldException;
51 import org.openhab.binding.insteon.internal.message.Msg;
52 import org.openhab.binding.insteon.internal.message.MsgListener;
53 import org.openhab.binding.insteon.internal.utils.Utils;
54 import org.openhab.core.io.transport.serial.SerialPortManager;
55 import org.openhab.core.thing.ChannelUID;
56 import org.openhab.core.types.Command;
57 import org.openhab.core.types.State;
58 import org.slf4j.Logger;
59 import org.slf4j.LoggerFactory;
60 import org.xml.sax.SAXException;
63 * A majority of the code in this file is from the openHAB 1 binding
64 * org.openhab.binding.insteonplm.InsteonPLMActiveBinding. Including the comments below.
66 * -----------------------------------------------------------------------------------------------
68 * This class represents the actual implementation of the binding, and controls the high level flow
69 * of messages to and from the InsteonModem.
71 * Writing this binding has been an odyssey through the quirks of the Insteon protocol
72 * and Insteon devices. A substantial redesign was necessary at some point along the way.
73 * Here are some of the hard learned lessons that should be considered by anyone who wants
74 * to re-architect the binding:
76 * 1) The entries of the link database of the modem are not reliable. The category/subcategory entries in
77 * particular have junk data. Forget about using the modem database to generate a list of devices.
78 * The database should only be used to verify that a device has been linked.
80 * 2) Querying devices for their product information does not work either. First of all, battery operated devices
81 * (and there are a lot of those) have their radio switched off, and may generally not respond to product
82 * queries. Even main stream hardwired devices sold presently (like the 2477s switch and the 2477d dimmer)
83 * don't even have a product ID. Although supposedly part of the Insteon protocol, we have yet to
84 * encounter a device that would cough up a product id when queried, even among very recent devices. They
85 * simply return zeros as product id. Lesson: forget about querying devices to generate a device list.
87 * 3) Polling is a thorny issue: too much traffic on the network, and messages will be dropped left and right,
88 * and not just the poll related ones, but others as well. In particular sending back-to-back messages
89 * seemed to result in the second message simply never getting sent, without flow control back pressure
90 * (NACK) from the modem. For now the work-around is to space out the messages upon sending, and
91 * in general poll as infrequently as acceptable.
93 * 4) Instantiating and tracking devices when reported by the modem (either from the database, or when
94 * messages are received) leads to complicated state management because there is no guarantee at what
95 * point (if at all) the binding configuration will be available. It gets even more difficult when
96 * items are created, destroyed, and modified while the binding runs.
98 * For the above reasons, devices are only instantiated when they are referenced by binding information.
99 * As nice as it would be to discover devices and their properties dynamically, we have abandoned that
100 * path because it had led to a complicated and fragile system which due to the technical limitations
101 * above was inherently squirrely.
104 * @author Bernd Pfrommer - Initial contribution
105 * @author Daniel Pfrommer - openHAB 1 insteonplm binding
106 * @author Rob Nielsen - Port to openHAB 2 insteon binding
109 public class InsteonBinding {
110 private static final int DEAD_DEVICE_COUNT = 10;
112 private final Logger logger = LoggerFactory.getLogger(InsteonBinding.class);
114 private Driver driver;
115 private Map<InsteonAddress, InsteonDevice> devices = new ConcurrentHashMap<>();
116 private Map<String, InsteonChannelConfiguration> bindingConfigs = new ConcurrentHashMap<>();
117 private PortListener portListener = new PortListener();
118 private int devicePollIntervalMilliseconds = 300000;
119 private int deadDeviceTimeout = -1;
120 private boolean driverInitialized = false;
121 private int messagesReceived = 0;
122 private boolean isActive = false; // state of binding
123 private int x10HouseUnit = -1;
124 private InsteonNetworkHandler handler;
126 public InsteonBinding(InsteonNetworkHandler handler, InsteonNetworkConfiguration config,
127 SerialPortManager serialPortManager, ScheduledExecutorService scheduler) {
128 this.handler = handler;
130 String port = config.getPort();
131 logger.debug("port = '{}'", Utils.redactPassword(port));
133 driver = new Driver(port, portListener, serialPortManager, scheduler);
134 driver.addMsgListener(portListener);
136 Integer devicePollIntervalSeconds = config.getDevicePollIntervalSeconds();
137 if (devicePollIntervalSeconds != null) {
138 devicePollIntervalMilliseconds = devicePollIntervalSeconds * 1000;
140 logger.debug("device poll interval set to {} seconds", devicePollIntervalMilliseconds / 1000);
142 String additionalDevices = config.getAdditionalDevices();
143 if (additionalDevices != null) {
145 DeviceTypeLoader instance = DeviceTypeLoader.instance();
146 if (instance != null) {
147 instance.loadDeviceTypesXML(additionalDevices);
148 logger.debug("read additional device definitions from {}", additionalDevices);
150 logger.warn("device type loader instance is null");
152 } catch (ParserConfigurationException | SAXException | IOException e) {
153 logger.warn("error reading additional devices from {}", additionalDevices, e);
157 String additionalFeatures = config.getAdditionalFeatures();
158 if (additionalFeatures != null) {
159 logger.debug("reading additional feature templates from {}", additionalFeatures);
160 DeviceFeature.readFeatureTemplates(additionalFeatures);
163 deadDeviceTimeout = devicePollIntervalMilliseconds * DEAD_DEVICE_COUNT;
164 logger.debug("dead device timeout set to {} seconds", deadDeviceTimeout / 1000);
167 public Driver getDriver() {
171 public boolean isDriverInitialized() {
172 return driverInitialized;
175 public boolean startPolling() {
176 logger.debug("starting to poll {}", driver.getPortName());
178 return driver.isRunning();
181 public void setIsActive(boolean isActive) {
182 this.isActive = isActive;
185 public void sendCommand(String channelName, Command command) {
187 logger.debug("not ready to handle commands yet, returning.");
191 InsteonChannelConfiguration bindingConfig = bindingConfigs.get(channelName);
192 if (bindingConfig == null) {
193 logger.warn("unable to find binding config for channel {}", channelName);
197 InsteonDevice dev = getDevice(bindingConfig.getAddress());
199 logger.warn("no device found with insteon address {}", bindingConfig.getAddress());
203 dev.processCommand(driver, bindingConfig, command);
205 logger.debug("found binding config for channel {}", channelName);
208 public void addFeatureListener(InsteonChannelConfiguration bindingConfig) {
209 logger.debug("adding listener for channel {}", bindingConfig.getChannelName());
211 InsteonAddress address = bindingConfig.getAddress();
212 InsteonDevice dev = getDevice(address);
214 logger.warn("device for address {} is null", address);
218 DeviceFeature f = dev.getFeature(bindingConfig.getFeature());
219 if (f == null || f.isFeatureGroup()) {
220 StringBuilder buf = new StringBuilder();
221 ArrayList<String> names = new ArrayList<>(dev.getFeatures().keySet());
222 Collections.sort(names);
223 for (String name : names) {
224 DeviceFeature feature = dev.getFeature(name);
225 if (feature != null && !feature.isFeatureGroup()) {
226 if (buf.length() > 0) {
233 logger.warn("channel {} references unknown feature: {}, it will be ignored. Known features for {} are: {}.",
234 bindingConfig.getChannelName(), bindingConfig.getFeature(), bindingConfig.getProductKey(),
239 DeviceFeatureListener fl = new DeviceFeatureListener(this, bindingConfig.getChannelUID(),
240 bindingConfig.getChannelName());
241 fl.setParameters(bindingConfig.getParameters());
244 bindingConfigs.put(bindingConfig.getChannelName(), bindingConfig);
247 public void removeFeatureListener(ChannelUID channelUID) {
248 String channelName = channelUID.getAsString();
250 logger.debug("removing listener for channel {}", channelName);
252 for (Iterator<Entry<InsteonAddress, InsteonDevice>> it = devices.entrySet().iterator(); it.hasNext();) {
253 InsteonDevice dev = it.next().getValue();
254 boolean removedListener = dev.removeFeatureListener(channelName);
255 if (removedListener) {
256 logger.trace("removed feature listener {} from dev {}", channelName, dev);
261 public void updateFeatureState(ChannelUID channelUID, State state) {
262 handler.updateState(channelUID, state);
265 public @Nullable InsteonDevice makeNewDevice(InsteonAddress addr, String productKey,
266 Map<String, Object> deviceConfigMap) {
267 DeviceTypeLoader instance = DeviceTypeLoader.instance();
268 if (instance == null) {
271 DeviceType dt = instance.getDeviceType(productKey);
275 InsteonDevice dev = InsteonDevice.makeDevice(dt);
276 dev.setAddress(addr);
277 dev.setProductKey(productKey);
278 dev.setDriver(driver);
279 dev.setIsModem(productKey.equals(InsteonDeviceHandler.PLM_PRODUCT_KEY));
280 dev.setDeviceConfigMap(deviceConfigMap);
281 if (!dev.hasValidPollingInterval()) {
282 dev.setPollInterval(devicePollIntervalMilliseconds);
284 if (driver.isModemDBComplete() && dev.getStatus() != DeviceStatus.POLLING) {
285 int ndev = checkIfInModemDatabase(dev);
286 if (dev.hasModemDBEntry()) {
287 dev.setStatus(DeviceStatus.POLLING);
288 Poller.instance().startPolling(dev, ndev);
291 devices.put(addr, dev);
293 handler.insteonDeviceWasCreated();
298 public void removeDevice(InsteonAddress addr) {
299 InsteonDevice dev = devices.remove(addr);
304 if (dev.getStatus() == DeviceStatus.POLLING) {
305 Poller.instance().stopPolling(dev);
310 * Checks if a device is in the modem link database, and, if the database
311 * is complete, logs a warning if the device is not present
313 * @param dev The device to search for in the modem database
314 * @return number of devices in modem database
316 private int checkIfInModemDatabase(InsteonDevice dev) {
318 InsteonAddress addr = dev.getAddress();
319 Map<InsteonAddress, ModemDBEntry> dbes = driver.lockModemDBEntries();
320 if (dbes.containsKey(addr)) {
321 if (!dev.hasModemDBEntry()) {
322 logger.debug("device {} found in the modem database and {}.", addr, getLinkInfo(dbes, addr, true));
323 dev.setHasModemDBEntry(true);
326 if (driver.isModemDBComplete() && !addr.isX10()) {
327 logger.warn("device {} not found in the modem database. Did you forget to link?", addr);
328 handler.deviceNotLinked(addr);
333 driver.unlockModemDBEntries();
337 public Map<String, String> getDatabaseInfo() {
339 Map<String, String> databaseInfo = new HashMap<>();
340 Map<InsteonAddress, ModemDBEntry> dbes = driver.lockModemDBEntries();
341 for (InsteonAddress addr : dbes.keySet()) {
342 String a = addr.toString();
343 databaseInfo.put(a, a + ": " + getLinkInfo(dbes, addr, false));
348 driver.unlockModemDBEntries();
352 public boolean reconnect() {
354 return startPolling();
358 * Everything below was copied from Insteon PLM v1
362 * Clean up all state.
364 public void shutdown() {
365 logger.debug("shutting down Insteon bridge");
368 RequestQueueManager.destroyInstance();
369 Poller.instance().stop();
374 * Method to find a device by address
376 * @param aAddr the insteon address to search for
377 * @return reference to the device, or null if not found
379 public @Nullable InsteonDevice getDevice(@Nullable InsteonAddress aAddr) {
380 InsteonDevice dev = (aAddr == null) ? null : devices.get(aAddr);
384 private String getLinkInfo(Map<InsteonAddress, ModemDBEntry> dbes, InsteonAddress a, boolean prefix) {
385 ModemDBEntry dbe = dbes.get(a);
389 List<Byte> controls = dbe.getControls();
390 List<Byte> responds = dbe.getRespondsTo();
392 Port port = dbe.getPort();
396 String deviceName = port.getDeviceName();
397 String s = deviceName.startsWith("/hub") ? "hub" : "plm";
398 StringBuilder buf = new StringBuilder();
399 if (port.isModem(a)) {
401 buf.append("it is the ");
405 buf.append(Utils.redactPassword(deviceName));
412 buf.append(" controls groups (");
413 buf.append(toGroupString(controls));
414 buf.append(") and responds to groups (");
415 buf.append(toGroupString(responds));
419 return buf.toString();
422 private String toGroupString(List<Byte> group) {
423 List<Byte> sorted = new ArrayList<>(group);
424 Collections.sort(sorted, new Comparator<>() {
426 public int compare(Byte b1, Byte b2) {
429 return i1 < i2 ? -1 : i1 == i2 ? 0 : 1;
433 StringBuilder buf = new StringBuilder();
434 for (Byte b : sorted) {
435 if (buf.length() > 0) {
438 buf.append(b & 0xFF);
441 return buf.toString();
444 public void logDeviceStatistics() {
445 String msg = String.format("devices: %3d configured, %3d polling, msgs received: %5d", devices.size(),
446 Poller.instance().getSizeOfQueue(), messagesReceived);
447 logger.debug("{}", msg);
448 messagesReceived = 0;
449 for (InsteonDevice dev : devices.values()) {
453 if (deadDeviceTimeout > 0 && dev.getPollOverDueTime() > deadDeviceTimeout) {
454 logger.debug("device {} has not responded to polls for {} sec", dev.toString(),
455 dev.getPollOverDueTime() / 3600);
461 * Handles messages that come in from the ports.
462 * Will only process one message at a time.
464 private class PortListener implements MsgListener, DriverListener {
466 public void msg(Msg msg) {
467 if (msg.isEcho() || msg.isPureNack()) {
471 logger.debug("got msg: {}", msg);
473 handleX10Message(msg);
475 handleInsteonMessage(msg);
480 public void driverCompletelyInitialized() {
481 List<String> missing = new ArrayList<>();
483 Map<InsteonAddress, ModemDBEntry> dbes = driver.lockModemDBEntries();
484 logger.debug("modem database has {} entries!", dbes.size());
485 if (dbes.isEmpty()) {
486 logger.warn("the modem link database is empty!");
488 for (InsteonAddress k : dbes.keySet()) {
489 logger.debug("modem db entry: {}", k);
491 Set<InsteonAddress> addrs = new HashSet<>();
492 for (InsteonDevice dev : devices.values()) {
493 InsteonAddress a = dev.getAddress();
494 if (!dbes.containsKey(a)) {
496 logger.warn("device {} not found in the modem database. Did you forget to link?", a);
497 handler.deviceNotLinked(a);
500 if (!dev.hasModemDBEntry()) {
502 logger.debug("device {} found in the modem database and {}.", a,
503 getLinkInfo(dbes, a, true));
504 dev.setHasModemDBEntry(true);
506 if (dev.getStatus() != DeviceStatus.POLLING) {
507 Poller.instance().startPolling(dev, dbes.size());
512 for (InsteonAddress k : dbes.keySet()) {
513 if (!addrs.contains(k)) {
514 logger.debug("device {} found in the modem database, but is not configured as a thing and {}.",
515 k, getLinkInfo(dbes, k, true));
517 missing.add(k.toString());
521 driver.unlockModemDBEntries();
524 if (!missing.isEmpty()) {
525 handler.addMissingDevices(missing);
528 driverInitialized = true;
532 public void disconnected() {
533 handler.bindingDisconnected();
536 private void handleInsteonMessage(Msg msg) {
537 InsteonAddress toAddr = msg.getAddr("toAddress");
538 if (!msg.isBroadcast() && !driver.isMsgForUs(toAddr)) {
539 // not for one of our modems, do not process
542 InsteonAddress fromAddr = msg.getAddr("fromAddress");
543 if (fromAddr == null) {
544 logger.debug("invalid fromAddress, ignoring msg {}", msg);
547 handleMessage(fromAddr, msg);
550 private void handleX10Message(Msg msg) {
552 int x10Flag = msg.getByte("X10Flag") & 0xff;
553 int rawX10 = msg.getByte("rawX10") & 0xff;
554 if (x10Flag == 0x80) { // actual command
555 if (x10HouseUnit != -1) {
556 InsteonAddress fromAddr = new InsteonAddress((byte) x10HouseUnit);
557 handleMessage(fromAddr, msg);
559 } else if (x10Flag == 0) {
560 // what unit the next cmd will apply to
561 x10HouseUnit = rawX10 & 0xFF;
563 } catch (FieldException e) {
564 logger.warn("got bad X10 message: {}", msg, e);
569 private void handleMessage(InsteonAddress fromAddr, Msg msg) {
570 InsteonDevice dev = getDevice(fromAddr);
572 logger.debug("dropping message from unknown device with address {}", fromAddr);
574 dev.handleMessage(msg);